SwiftUIを楽にするSwift 5.3の新機能
昨日 Swift 5.3 がリリースされました( Xcode 12 に含まれます)。マイナーバージョンアップということもあり、言語自体に大きな変化があるわけではありませんが、 SwiftUI にとっては大きなアップデート です。なぜなら、
- クロージャ式の中のほとんどの
self.
を省略できるようになる -
@ViewBuilder
でif let
やswitch
が使えるようになる
からです。
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
クラスは、 Clock
と Timer
が互いに参照しあっているため、他から参照されていなくても解放されません。お互いの参照をカウントするため、参照カウントが 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
インスタンスを即座に捨てても、どこからも参照されていない状態でこの Clock
と Timer
インスタンスは生き続けます。これはメモリリークです。
_ = Clock() // これでも `Clock` と `Timer` は解放されない
この場合の正しい実装は、 [weak self]
を使って self
を weak
キャプチャ(弱参照)することです。
// 循環参照を回避する実装
// 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
に渡したクロージャ式は self
を weak
キャプチャしているので、 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 では通常 View
を struct
(値型)として実装します。そのため、クロージャ式の中の self.
はほとんどのケースで省略できるようになります。
@ViewBuilder
での if let
や swifch
の利用
// 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 let
や switch
を扱うことができませんでした。 Swift 5.3 ではこれが可能になります。
Function Builder とは
Function Builder は宣言的に値を組み立てるための仕組みです。 @ViewBuilder
の他にも、 Function Builder を使えば様々なものを宣言的に記述できるようになります。
たとえば、↓は Dictionary
を宣言的に組み立てる例です。通常の Dictionary リテラルでは if
で分岐することができませんが、この例では isFoo
が true
の場合だけ "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 let
と switch
が使えるようになりました。正式に Function Builder が実装されれば、最終的には for
文なども利用できるようになる予定です。そうすれば、 SwiftUI で ForEach
を for
文で書けるようになるかもしれません。
@ViewBuilder
で if let
と switch
を使う
せっかく 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 では @ViewBuilder
で if 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 はエラーを返すことも多いでしょう。 fetchValue
の completion
ハンドラーが 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 })
}
}
}
Optional
と if let
はもちろん、 Swift では enum
を使うことが多く、 switch
の利用機会も少なくありません。 @ViewBuilder
で if let
と switch
が使えないことは SwiftUI のコードを書く上で大きなストレスでした。 Swift 5.3 でそれらが解消されたことで、快適な SwiftUI ライフを送れるようになるでしょう。
まとめ
Swift 5.3 では、 SwiftUI のコードを書く際に↓が実現されます。
- クロージャ式の中のほとんどの
self.
を省略できる -
@ViewBuilder
でif let
やswitch
が使える
Swift 5.3 で快適な SwiftUI ライフを楽しんで下さい!
Discussion