はじめに
Swift5.3がリリース最終段階に入ったらしいので、変更が入った機能についてまとめた。
変更点は下記ページのProposalを元に確認した。
https://apple.github.io/swift-evolution/#?version=5.3
(理解が異なる箇所があればご指摘ください)
Swift5.3
リリースプロセス
スナップショット
変更サマリ
- 品質とパフォーマンスの強化
- Swiftが利用可能できるプラットフォームの拡張
- Windowsサポート
- 対応Linuxディストリビューションの追加
- 0263-string-uninitialized-initializer:初期化されていないバッファを使用するStringのイニシャライザを追加
- 0266-synthesized-comparable-for-enumerations:定義値のみでのenumのComparable準拠
- 0267-where-on-contextually-generic:関数定義に対するGenericsの条件指定
- 0268-didset-semantics:didSetでの不要なゲッター呼び出し見直し
- 0269-implicit-self-explicit-capture:不必要なselfの省略
- 0270-rangeset-and-collection-operations:非連続な位置を参照するコレクション操作の追加
- 0271-package-manager-resources :Swift Package Managerでのリソースファイル対応
- 0272-swiftpm-binary-dependencies:Swift Package Managerでのバイナリパッケージ利用対応
- 0273-swiftpm-conditional-target-dependencies:プラットフォームやConfigurationを条件にしたSPMターゲットの構成
- 0276-multi-pattern-catch-clauses:複数パターンcatch句のサポート
- 0277-float16:Float16の標準ライブラリ追加
- 0278-package-manager-localized-resources:Swift Package Managerリソースのローカライズ対応
- 0279-multiple-trailing-closures:複数TrailingClosure記述方法の改善
- 0280-enum-cases-as-protocol-witnesses:enumのプロトコル準拠判定の改善
- 0281-main-attribute:アプリのエントリーポイントを指定できるmain属性の追加
try! Swift5.3
Dockerのイメージが公式で提供されているので、簡易的にこれを使って試してみた。
https://hub.docker.com/_/swift/
$ mkdir swift5.3; cd $_;
$ touch main.swift # 好みのエディタで編集
$ docker pull swiftlang/swift:nightly-5.3-bionic
$ docker run --rm --privileged -v $(pwd):/swift5.3 -it swiftlang/swift:nightly-5.3-bionic /bin/bash
root@a526a6b578b6:/# swift swift5.3/main.swift
[SE-0263] Add a String Initializer with Access to Uninitialized Storage
SE-0263:初期化されていないバッファを使用するStringのイニシャライザを追加
新しいイニシャライザを使うことで、UnsafeMutableBufferPointerを自前で取り扱わずに、初期化されていないバッファを使用したStringの生成ができるようになった。
// Swift5.3
let myCocoaString = NSString("The quick brown fox jumps over the lazy dog") as CFString
var myString = String(unsafeUninitializedCapacity: CFStringGetMaximumSizeForEncoding(myCocoaString, …)) { buffer in
var initializedCount = 0
CFStringGetBytes(
myCocoaString,
buffer,
…,
&initializedCount
)
return initializedCount
}
// myString == "The quick brown fox jumps over the lazy dog"
[SE-0266] Synthesized Comparable
conformance for enum
types
SE-0266:定義値のみでのenumのComparable準拠
Swift5.2以前はenumの値同士を比較したい場合、Comparableに準拠させた上で、明示的に比較メソッドを実装しておく必要があった。
// Swift5.2
enum Size: Comparable {
case small
case medium
case large
private var comparisonValue: Int {
switch self {
case .small: return 0
case .medium: return 1
case .large: return 2
}
}
// この実装が必要
static func < (lhs: Size, rhs: Size) -> Bool {
lhs.comparisonValue < rhs.comparisonValue
}
}
Size.small < Size.medium // true
Size.medium < Size.large // true
Size.large < Size.small // false
let sizes: [Size] = [.medium, .small, .large]
sizes.sorted() // [Size.small, Size.medium, Size.large]
Swift5.3からは該当メソッドを実装しなかった場合、定義順で比較されるようになり、後に定義された方が大きいとみなされるようになる。
// Swift5.3
enum Size: Comparable {
case small
case medium
case large
}
let sizes: [Size] = [.medium, .small, .large]
sizes.sorted() // [Size.small, Size.medium, Size.large]
値付きenumの場合も対応可能で、この場合はassociatedValueもComparableに準拠していることが求められる。
同一のcaseだが値が異なるものの場合、比較はペイロードの値を元に行われる。
// Swift5.3
enum Category: Comparable {
case tops(Size)
case bottoms(Size)
}
let categories: [Category] = [.bottoms(.medium), .tops(.large), .bottoms(.small), .tops(.medium)]
categories.sorted() // [Category.tops(Size.medium), Category.tops(Size.large), Category.bottoms(Size.small), Category.bottoms(Size.medium)]
所感
これを活用することでボイラープレートコードをより少なくできそう。
一方でカジュアルに定義の順番を変更しちゃったりするとバグる可能性があるので注意はした方がいい。
[SE-0267] where
clauses on contextually generic declarations
SE-0267:関数定義に対するGenericsの条件指定
下記のコード例に関して、「ElementがComparableに適合している場合のみsortedメソッドを実行できる」という挙動を実現したい場合、Swift5.2以前では対象クラスのextensionに対してwhere句で条件指定をする必要があった。
// Swift5.2
struct Stack<Element> {
private var elements = [Element]()
mutating func push(_ obj: Element) {
elements.append(obj)
}
mutating func pop(_ obj: Element) -> Element? {
elements.popLast()
}
}
// extensionでGenericsに関する条件を指定する
extension Stack where Element: Comparable {
func sorted() -> [Element] {
elements.sorted()
}
}
これが、Swift5.3からは直接関数に対して where句
でGenericsの条件指定を加えられるようにもなった。
// Swift5.3
extension Stack {
// extensionにではなく、関数にwhere句を付与できる
func sorted() -> [Element] where Element: Comparable {
elements.sorted()
}
}
これにより、extensionで切り出さなくても直接class, struct内にGenericsの条件を指定した関数を定義することも可能になる。
// Swift5.3
struct Stack<Element> {
private var elements = [Element]()
mutating func push(_ obj: Element) {
elements.append(obj)
}
mutating func pop(_ obj: Element) -> Element? {
elements.popLast()
}
// extensionで切り出さなくても条件指定をした関数を宣言できる
func sorted() -> [Element] where Element: Comparable {
elements.sorted()
}
}
所感
Extensionでは区切らない方が関数を意味的にグルーピングしやすい場合などに便利そう。
使い方によっては冗長性を排除できる気がするので、積極的に使っていきたい。
[SE-0268] Refine didSet
Semantics
SE-0268:didSetでの不要なゲッター呼び出し見直し
Swift5.2以前では、プロパティにdidSetを追加した場合、値に変更があると常にoldValueの値が内部的に参照されるようになっていた。
これにより、意図せずパフォーマンスを低下させてしまうコードを実装してしまう恐れがあった。
// swift5.2
struct Container {
var items: [Int] = .init(repeating: 1, count: 100) {
didSet {
// 実装としてoldValueを参照していなくても、内部的に参照されていた
}
}
mutating func update() {
(0..<items.count).forEach { items[$0] = $0 + 1 }
}
}
var container = Container()
container.update() // この時点でitemsのoldValueを参照するために100回配列のコピーが行われる
Swift5.3ではこの挙動が改善され、didSetの内部でoldValueを明示的に参照しない限り、内部的にも参照されないようになった。
所感
oldValueが常に参照されていることを意識せずに実装していたので、こういった改善はありがたい。
また、もともとdidSet内でoldValueが呼び出されることに依存しているような処理があった場合は、挙動が変わってしまうことになるため注意が必要(そもそもそういった実装はよくないが)。
[SE-0269] Increase availability of implicit self
in @escaping
closures when reference cycles are unlikely to occur
SE-0269:不必要なselfの省略
Swift5.2では循環参照の可能性がない場合でも、@escaping
クロージャを受け渡す場合には必ずself.
を記述する必要があった。
func someFunction(closure: @escaping () -> Void) {
closure()
}
// Swift5.2
struct Main {
func run() {
someFunction {
// 必ずself.で参照する必要がある
self.helloWorld()
}
}
func helloWorld() {
print("Hello World")
}
}
Main().run()
これがSwift5.3では、循環参照の可能性がないと判断できる場合にはself.
を省略することができるようになった。
selfが値型の場合は、循環参照の可能性がないため、self.
をそのまま省略できる。
// Swift5.3
// 値型
struct Main {
func run() {
someFunction {
// selfが不要
helloWorld()
}
}
}
selfが参照型の場合は、[self]
をつけるとselfを省略できるようになり、循環参照の可能性がある場合は従来通り[weak self]
を付与してselfを記述して利用する形になる。
// Swift5.3
// 参照型
class Main {
func run() {
// [self]をつければself.が省略可能
someFunction { [self] in
helloWorld()
}
}
}
つまり、[weak self]
にするべきか[self]
にするべきか、一度決めてしまえばあとはself.
を書かなくてよくなるため、循環参照を意識させつつも不要なselfは書かなくてよくなることになる。
所感
循環参照の可能性を強制的に考慮させる仕組みは残しつつ、不要なselfの記述も省略もできるのでとても良い。
self.の記述がたびたび必要になるSwiftUIのソースコードもきれいになりそう。
[SE-0270] Add Collection Operations on Noncontiguous Elements
SE-0270:非連続な位置を参照するコレクション操作の追加
RangeSet
という配列内の非連続な値の位置を表す型が追加される。
また、これをベースに配列を操作するオペレーションも追加される。
Collectionにsubranges
メソッドが追加されており、引数として条件のクロージャを渡すことでRangeSetオブジェクトが取得できる(返却されるのは値自体ではない)。
// Swift5.3
var numbers = Array(1...6)
let multipleOf2 = numbers.subranges(where: { $0.isMultiple(of: 2) }) // RangeSet(1..<2, 3..<4, 5..<6)
let multipleOf3 = numbers.subranges(where: { $0.isMultiple(of: 3) }) // RangeSet(2..<3, 5..<6)
RangeSetは集合を操作するメソッドも使える。
// 和集合
multipleOf2.union(multipleOf3) // RangeSet(1..<4, 5..<6)
// 積集合
multipleOf2.intersection(multipleOf3) // RangeSet(5..<6)
元の配列に対して、subscriptでRangeSetを渡すとスライス(DiscontiguousSlice) が取得できる。
これに対してメソッドを呼び出したり、値を取り出したりしてあげることで、対象のデータのみを取り扱うことも可能。
let count = numbers[multipleOf2].count // 3
let total = numbers[multipleOf2].reduce(0, +) // 12
let values = multipleOf3.ranges.flatMap { numbers[$0] } // [3, 6]
let array = Array(numbers[multipleOf3]) // [3, 6]
また、RangeSetに含まれる要素をまとめて指定ポイントに移動する便利関数なども用意されている
numbers.moveSubranges(multipleOf2, to: numbers.endIndex) // 3..<6(移動先のRangeが返却される)
numbers // [1, 3, 5, 2, 4, 6]
所感
複雑な配列操作をしたいケースなど、これを使うと泥臭い実装をしなくて済む時がありそう。
[SE-0271] Package Manager Resources
SE-0271 : Swift Package Managerでのリソースファイル対応
これまでSwift Package Managerでできなかった画像、オーディオ、Storyboard、JSONなどのリソースファイルをバンドルできるようになった。
targetにresourcesが追加されており、これにResourceのインスタンスを渡すことで設定する。
public static func target(
name: String,
dependencies: [Target.Dependency] = [],
path: String? = nil,
exclude: [String] = [],
sources: [String]? = nil,
resources: [Resource]? = nil, // <=== NEW
publicHeadersPath: String? = nil,
cSettings: [CSetting]? = nil,
cxxSettings: [CXXSetting]? = nil,
swiftSettings: [SwiftSetting]? = nil,
linkerSettings: [LinkerSetting]? = nil
) -> Target
/// Represents an individual resource file.
public struct Resource {
/// Apply the platform-specific rule to the given path.
///
/// Matching paths will be processed according to the platform for which this
/// target is being built. For example, image files might get optimized when
/// building for platforms that support such optimizations.
///
/// By default, a file will be copied if there is no specialized processing
/// for its file type.
///
/// If path is a directory, the rule is applied recursively to each file in the
/// directory.
public static func process(_ path: String) -> Resource
/// Apply the copy rule to the given path.
///
/// Matching paths will be copied as-is and will be at the top-level
/// in the bundle. The structure is retained for if path is a directory.
public static func copy(_ path: String) -> Resource
}
SPMはBundleのextensionとしてmodule
を提供するので、これを使って実行時にリソースにアクセスできる。
(このextensionはモジュールごとinternalで提供されるので、各モジュールで実装が干渉することはない。)
extension Bundle {
static let module: Bundle = { ... }()
}
// DefaultSettings.plistへのパスを取得する
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")
// モジュールに含まれるリソースを利用して画像を取得する
let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))
所感
UI系のライブラリなどもをSPMで提供しやすくなってそう。
[SE-0272] Package Manager Binary Dependencies
SE-0272:Swift Package Managerでのバイナリパッケージ利用対応
Swift Package Managerでソースコード自体は公開せずにパッケージを提供できるようになる。
これにより、FirebaseSDKなどのクローズドSDKがSPMに対応できるようになる。
※ この機能は、最初のフェーズではAppleのプラットフォームに限定し、将来的に拡張しておく模様。
let package = Package(
name: "SomePackage",
platforms: [
.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2),
],
products: [
.library(name: "SomePackage", targets: ["SomePackageLib"])
],
targets: [
.binaryTarget(
name: "SomePackageLib",
url: "https://github.com/some/package/releases/download/1.0.0/SomePackage-1.0.0.zip",
checksum: "839F9F30DC13C30795666DD8F6FB77DD0E097B83D06954073E34FE5154481F7A"
),
.binaryTarget(
name: "SomeLibOnDisk",
path: "artifacts/SomeLibOnDisk.zip"
)
]
)
ソースターゲットとバイナリターゲットを混在させることもできる。
所感
この流れでCocoapods, Carthageで管理しているライブラリもSPMに移行できたら嬉しい。
[SE-0273] Package Manager Conditional Target Dependencies
SE-0273:プラットフォームやConfigurationを条件にしたSPMターゲットの構成
以下のように、条件ごとに設定を変えるような対応ができるようになる
- macOS向けとLinux向けでフレームワークを使い分ける
- debugビルドの時のみ追加のフレームワークを組み込む
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "BestPackage",
dependencies: [
.package(url: "https://github.com/pureswift/bluetooth", .branch("master")),
.package(url: "https://github.com/pureswift/bluetoothlinux", .branch("master")),
],
targets: [
.target(
name: "BestExecutable",
dependencies: [
.product(name: "Bluetooth", condition: .when(platforms: [.macOS])),
.product(name: "BluetoothLinux", condition: .when(platforms: [.linux])),
.target(name: "DebugHelpers", condition: .when(configuration: .debug)),
]
),
.target(name: "DebugHelpers")
]
)
所感
1つのパッケージでより汎用的に使えるライブラリが提供できて便利そう。
[SE-0276] Multi-Pattern Catch Clauses
SE-0276:複数パターンcatch句のサポート
Swift5.2では例外ハンドリング時のcatch句には、単一のパターンとwhere句のみしか設定できなかった。
そのため、各エラー発生時に同一の処理を行いたい場合にも、処理を重複して記述する必要があった。
// Swift5.2
enum SampleError: Error {
case badRequest(String)
case unAuthorized(String)
case internalServerError(String)
case serviceUnavailable(String)
}
do {
try doSomething()
} catch SampleError.badRequest(let message) {
handleClientError(with: message)
} catch SampleError.unAuthorized(let message) {
handleClientError(with: message)
} catch SampleError.internalServerError(let message) {
handleServerError(with: message)
} catch SampleError.serviceUnavailable(let message) {
handleServerError(with: message)
}
Swift5.3ではcatch句の文法が拡張され、パターンをカンマで区切ることでエラーハンドリングの共通化ができるようになった。
// Swift5.3
do {
try doSomething()
} catch SampleError.badRequest(let message),
SampleError.unAuthorized(let message) {
handleClientError(with: message)
} catch SampleError.internalServerError(let message),
SampleError.serviceUnavailable(let message) {
handleServerError(with: message)
}
[SE-0277] Float16
SE-0277:Float16の標準ライブラリ追加
所感
グラフィックスプログラミングと機械学習で一般的に使用されている型のようなので、界隈のひとには嬉しい変更かもしれない。
[SE-0278] Package Manager Localized Resources
SE-0278:SPMリソースのローカライズ対応
ローカライズされたリソースを、<IFTF言語タグ>.lproj
ディレクトリに配置することで、ロケーションに応じたリソースの使い分けができるようになる。
例として、以下のようなローカライズされたリソースが存在する場合、Package.swiftで指定する際は<IFTF言語タグ>.lproj
を除いたパスで指定する。
- Resources/en.lproj/Icon.png
- Resources/fr.lproj/Icon.png
let package = Package(
name: "BestPackage",
defaultLocalization: "en",
targets: [
.target(name: "BestTarget", resources: [
.process("Resources/Icon.png"),
])
]
)
またSPMはApple固有プラットフォームのリソース対応のために、Base.lproj
配下のリソースもBaseInternationalization
を使用するものとして認識する。
[SE-0279] Multiple Trailing Closures
SE-0279:複数TrailingClosure記述方法の改善
Swiftでは、末尾の引数にクロージャを渡すコードを、明快で読みやすくできるTrailing Closureという機能が搭載されている。
// Swift5.2
// Trailing Closureを使わない
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
})
// Trailing Closureを使うと、末尾のクロージャのラベルを省略した上で記述を外に出すことができる
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
しかし、もし末尾に連続でクロージャが渡されると、それらが何を表すものかが不明瞭になり、ネストも深くなってしまう課題があった。
// Swift5.2
// Trailing Closureを使わない
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
// Trailing Closureを使う
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
// このクロージャが何なのかが不明瞭
self.view.removeFromSuperview()
}
そのため、Swift5.2以前では以下のように使い分ける対応をすることが多かった。
- 末尾のクロージャが単一 → Trailing Closureを利用する
- 末尾のクロージャが連続 → Trailing Closureを利用しない
Swift5.3ではこの記述方法に改善が入り、1つめのクロージャには既存と同様の省略記法を適用し、後続するクロージャはラベルを付与した上で表現するといった記述が可能になった。(これまでと同様の記述方法も可能)
// Swift 5.3
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
} completion: { _ in
self.view.removeFromSuperview()
}
所感
関数の定義方法を工夫することで読みやすいコードにできそうな気がする。
メソッド名に対して関連度が高いクロージャを最初に指定して、後続するクロージャは追加の意味合いが強いものを持ってくると読みやすくなりそう。
// Swift5.3
func listen(onReceive: () -> Void, onCancel: () -> Void) {}
listen {
print("onReceive")
} onCancel: {
print("onCancel")
}
[SE-0280] Enum cases as protocol witnesses
SE-0280:enumのプロトコル準拠判定の改善
下記のJSONDecodingError定義は、struct, enumでそれぞれ全く同じように呼び出すことができる。
struct JSONDecodingError {
static var fileCorrupted: Self {}
static func keyNotFound(_ key: String) -> Self {}
}
enum JSONDecodingError {
case fileCorrupted
case keyNotFound(_ key: String)
}
let error1 = JSONDecodingError.fileCorrupted
let error2 = JSONDecodingError.keyNotFound("hoge")
しかし、これを下記のDecodingErrorプロトコルに準拠させようとすると、Swift5.2の場合ではstructで定義した場合のみプロトコルに準拠していると見なされ、enumの場合は不適合と見なされてしまっていた。
// Swift5.2
protocol DecodingError {
static var fileCorrupted: Self { get }
static func keyNotFound(_ key: String) -> Self
}
// o
struct JSONDecodingError: DecodingError { ... }
// x
enum JSONDecodingError: DecodingError { ... }
Swift5.3ではこれに改善が入り、以下のルールに合致した場合、プロトコルに適合していると見なされるようになった。
- 値を持たないenumのケースは、列挙型もしくはSelfを保持する静的なget-onlyプロパティと関連付けられる
- 値を持つenumのケースは、associatedValueと同じ値を引数に取り、列挙型もしくはSelfを返す静的関数と関連付けられる
// Swift5.3
protocol DecodingError {
static var fileCorrupted: Self { get }
static func keyNotFound(_ key: String) -> Self
}
// OK
enum JSONDecodingError: DecodingError {
case fileCorrupted
case keyNotFound(_ key: String)
}
[SE-0280] @main
: Type-Based Program Entry Points
SE-0281:アプリのエントリーポイントを指定できる@main属性の追加
Swiftはmain.swiftを自動的にトップレベルのコードとみなすため、Swift5.2以前では以下のような実装をすることでエントリーポイントを定義していた。
// Swift5.2
struct MyApp {
func run() {
print("Running!")
}
}
let app = MyApp()
app.run()
Swift5.3では、main.swiftを追加していない場合、@main
属性が付与されたクラスのstatic main関数がエントリーポイントと見なされるようになる。
これはモジュールの任意のソースファイルに付与することができる。
// Swift5.3
@main
struct MyApp {
static func main() {
print("Running!")
}
}
@main
属性を使用するには下記の制限がある。
- main.swiftが存在するアプリでは
@main
属性は使用できない(既存と同様の動作) - 複数の
@main
属性は付与できない
所感
ライブラリの実装者などは、protocol extensionなどでstatic main関数をあらかじめ定義しておけば、利用者側のエントリーポイントに該当のプロトコルの準拠+@main属性の付与をさせるだけで、望んだ動作をさせられるのは良さそう。