SwiftUIを楽にするSwift 5.3の新機能

14 min読了の目安(約8600字TECH技術記事
Likes65

昨日 Swift 5.3 がリリースされました( Xcode 12 に含まれます)。マイナーバージョンアップということもあり、言語自体に大きな変化があるわけではありませんが、 SwiftUI にとっては大きなアップデート です。なぜなら、

  • クロージャ式の中のほとんどの self. を省略できるようになる
  • @ViewBuilderif letswitch が使えるようになる

からです。

self. の省略

// BEFORE (Swift 5.2)
Button("OK") {
    self.isEnabled = true
}
// AFTER (Swift 5.3)
Button("OK") {
    isEnabled = true
}

Swift ではこれまで @escaping な引数にクロージャ式を渡す場合、クロージャ式の中から self のメンバにアクセスしようとすると、明示的に self. を書く必要がありました。しかし、 Swift 5.3 では SwiftUI のコードを書く多くのケースで self. を省略できるようになります。

そもそも何のために self. を書かないといけなかったのか

そもそも self. が強制されていた理由は何でしょうか。それは 循環参照によるメモリリーク を避けるためです。

たとえば、次の Clock クラスは、 ClockTimer が互いに参照しあっているため、他から参照されていなくても解放されません。お互いの参照をカウントするため、参照カウントが 0 にならないからです。

// 循環参照の例

// 1 秒ごとに現在の秒数を表示するクラス
final class Clock {
    private var timer: Timer!
    private var seconds: Int = 0
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0,
                repeats: true) { _ in
            print(self.seconds)
            self.seconds += 1
        }
    }
    deinit { timer.invalidate() }
}

↓のように、たとえ生成した Clock インスタンスを即座に捨てても、どこからも参照されていない状態でこの ClockTimer インスタンスは生き続けます。これはメモリリークです。

_ = Clock() // これでも `Clock` と `Timer` は解放されない

この場合の正しい実装は、 [weak self] を使って selfweak キャプチャ(弱参照)することです。

// 循環参照を回避する実装

// 1 秒ごとに現在の秒数を表示するクラス
final class Clock {
    private var timer: Timer!
    private var seconds: Int = 0
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0,
                repeats: true) { [weak self] _ in
            guard let self = self else { return }
            print(self.seconds)
            self.seconds += 1
        }
    }
    deinit { timer.invalidate() }
}

この場合 Timer に渡したクロージャ式は selfweak キャプチャしているので、 self の参照カウントを増やしません。そのため、外部から Clock インスタンスへの参照がなくなれば Clock インスタンスは解放され、そうすると Clock インスタンスが保持していた Timer への参照もなくなるので Timer インスタンスも解放されます。

もしこのケースで self. が強制されなかった場合、どうなるでしょうか。たとえば、次のコードの中には一つも self が現れていません。注意深くコードを書かなければ、 self をキャプチャしていることを見逃し、循環参照によるメモリリークを生んでしまうかもしれません。

// 意図せず self をキャプチャする例

// 1 秒ごとに現在の秒数を表示するクラス
final class Clock {
    private var timer: Timer!
    private var seconds: Int = 0
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0,
                repeats: true) { _ in
            print(seconds) // ここで self をキャプチャ
            seconds += 1 // ここで self をキャプチャ
        }
    }
    deinit { timer.invalidate() }
}

self. が強制されることで、この self は循環参照によるメモリリークを引き起こさないかと、一歩立ち止まって考えることができるわけです。

メモリリークしないケース: @nonescaping

@escaping でない@nonescaping な)引数にクロージャ式を渡す場合、循環参照によるメモリリークを気にする必要はありません。

たとえば、次のコードでは forEach に渡すクロージャ式の中で Kuku (九九)クラスの dan (段)プロパティにアクセスしていますが、 self.dan のように self. を書く必要はありません。

// self. が不要な例

// 与えられた九九の段( dan )を表示するクラス
final class Kuku {
    let dan: Int
    init(dan: Int) { self.dan = dan }
    func show() {
        (1 ... 9).forEach { print(dan * $0) } // self は不要
    }
}

なぜなら、 forEach の引数は @escaping でない からです。関数やメソッドの @escaping でない引数にクロージャが渡された場合、その関数やメソッドは渡されたクロージャをプロパティ等にとっておくことはできません。渡されたクロージャへの参照は、その関数やメソッドの実行が終わるとただちに破棄されます。

そのため、たとえクロージャ式が self をキャプチャしていたとしても、関数やメソッドを抜けたらそのクロージャ式は即時解放されるため、クロージャ式から self への参照も破棄されます。循環参照が残ってメモリリークすることはありません。

そのような理由から、これまでも @escaping でない 引数にクロージャ式を渡す場合は、そのクロージャ式中での self. を省略することができました。

メモリリークしないケース: 値型

@escaping なクロージャ式を書く場合でも、循環参照によるメモリリークの恐れがないことがあります。それは self が値型の場合です。

値型は参照型と違ってそもそも参照することができないので、循環参照が起こるおそれはありません。しかしそれにも関わらず、たとえ self が値型の場合でも、これまでは @escaping なら self. を書くことが強制されていました。

// Swift 5.2
struct ContentView: View {
    @State var isEnabled: Bool = false
    
    var body: some View {
        Button("OK") {
            self.isEnabled = true // self が必要
        }
    }
}

Swift 5.3 ではこの制約が取り払われ、 self が値型の場合には self. を書く必要がなくなりました。

// Swift 5.3
struct ContentView: View {
    @State var isEnabled: Bool = false
    
    var body: some View {
        Button("OK") {
            isEnabled = true // self は不要
        }
    }
}

SwiftUI では通常 Viewstruct (値型)として実装します。そのため、クロージャ式の中の self. はほとんどのケースで省略できるようになります。

@ViewBuilder での if letswifch の利用

// BEFORE (Swift 5.2)
VStack {
    Text("Result:")
    if value != nil {
        Text("\(value!)")
    }
}
// AFTER (Swift 5.3)
VStack {
    Text("Result:")
    if let value = self.value {
        Text("\(value)")
    }
}

SwiftUI では body の中に宣言的に View を記述することができます。これは body@ViewBuilder が付与されているからです。 @ViewBuilder は Function Builder という言語仕様を用いて実装されています。

Swift 5.2 までの Function Builder では if letswitch を扱うことができませんでした。 Swift 5.3 ではこれが可能になります。

Function Builder とは

Function Builder は宣言的に値を組み立てるための仕組みです。 @ViewBuilder の他にも、 Function Builder を使えば様々なものを宣言的に記述できるようになります。

たとえば、↓は Dictionary を宣言的に組み立てる例です。通常の Dictionary リテラルでは if で分岐することができませんが、この例では isFootrue の場合だけ "c": 5 というエントリーが含まれるように分岐しています。

// Dictionary のイニシャライザに Function Builder を用いた例
let dictionary: [String: Int] =  .init {
    [
        "a": 2,
        "b": 3,
    ]
    if isFoo { ["c": 5] } // 分岐することが可能
}

こんなことができるのは、 Dictionary のイニシャライザに @DictionaryBuilder という Function Builder を付与しているからです。この @DictionaryBuilder は標準ライブラリに含まれるものではなく、僕が実装してライブラリにしたものです。誰でも Function Builder を使ってこの @DictionaryBuilder のようなものを作ることができます。

実はこの Function Builder はまだ( 2020 年 9 月 18 日時点では)正式には Swift の言語仕様として採用されていません。現時点で最新の Proposal でまさに今議論が行われ、 Swift Core Team が結論を下そうとしているところです。

最終的に承認されるのは間違いないでしょうが、正式に組み込まれるのは Swift 5.4 か Swift 6 になるでしょう。

まだ承認されていないにも関わらず、 Function Builder は Swift 5.1 から使うことができます。これは、 SwiftUI のために非公式機能として組み込まれたからです。 Function Builder 自体は 2019 年 1 月から議論されており、昨年 SwiftUI のリリースと同時に Swift に組み込まれました。

実験的に組み込まれたため Function Builder として提案されている一部の機能しか提供されておらず、これまでは通常の( if let でない) if 文しか使うことができませんでした。 Swift 5.3 では、それに加えて if letswitch が使えるようになりました。正式に Function Builder が実装されれば、最終的には for 文なども利用できるようになる予定です。そうすれば、 SwiftUI で ForEachfor 文で書けるようになるかもしれません。

@ViewBuilderif letswitch を使う

せっかく Swift には Optional があるのに、これまでは @ViewBuilder の中で if let が使えず、次のように ! で Forced Unwrapping しないといけませんでした。

// Swift 5.2
struct ContentView: View {
    @State var value: Int?

    var body: some View {
        VStack {
            Text("Result:")
            if value != nil { // if let にできない
                Text("\(value!)") // ! が必要
            }
        }
        .onAppear {
            fetchValue(completion: { self.value = $0 })
        }
    }
}

Swift 5.3 では @ViewBuilderif let が使えるので次のように書けます。

// Swift 5.3
struct ContentView: View {
    @State var value: Int?

    var body: some View {
        VStack {
            Text("Result:")
            if let value = self.value { // if let
                Text("\(value)") // ! は不要
            }
        }
        .onAppear {
            fetchValue(completion: { value = $0 })
        }
    }
}

また、 switch を使うこともできるようになりました。

↑の例では fetchValue は必ず成功しますが、 fetchValue のような非同期 API はエラーを返すことも多いでしょう。 fetchValuecompletion ハンドラーが Int の代わりに Result<Int, Error> を受け取るケースでは、 switch を使って次のように分岐できます。

// Swift 5.3
struct ContentView: View {
    @State private var value: Result<Int, Error>?
    
    var body: some View {
        VStack {
            Text("Result:")
            if let value = self.value {
                switch value { // switch も使える
                case .success(let value):
                    Text("\(value)")
                case .failure(_):
                    Text("Error")
                }
            }
        }
        .onAppear {
            fetchValue(completion: { value = $0 })
        }
    }
}

Optionalif let はもちろん、 Swift では enum を使うことが多く、 switch の利用機会も少なくありません。 @ViewBuilderif letswitch が使えないことは SwiftUI のコードを書く上で大きなストレスでした。 Swift 5.3 でそれらが解消されたことで、快適な SwiftUI ライフを送れるようになるでしょう。

まとめ

Swift 5.3 では、 SwiftUI のコードを書く際に↓が実現されます。

  • クロージャ式の中のほとんどの self. を省略できる
  • @ViewBuilderif letswitch が使える

Swift 5.3 で快適な SwiftUI ライフを楽しんで下さい!