SwiftUIでコードを書いていると、クロージャでViewのself
を頻繁に使うと思います。大抵のiOSアプリ開発者はself
を書かなければいけない場面にのみself
を利用しているので、循環参照しないようにweak
もしくはunowned
にすることを考慮したくなりますが、SwiftUIのViewはそこは考慮しないでも良いようです。この記事はその理由などを書いています。
なぜSwiftUIでself
が登場しても循環参照しないのか
本題です。SwiftUIでself
を頻繁に使う例として、Buttonのactionクロージャでself
を使う例を利用します。
画面としては次のようなボタンだけのContentViewを考えます。
import SwiftUI
struct ContentView: View {
var body: some View {
Button(action: { /* [weak self] にそもそもできない */
self.foo()
}) {
Text("ここを押しなさい...")
}
}
func foo() {
print("foo")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
このようになぜself
をそのまま使っても循環参照しないのかについて箇条書きにします。
- ContentViewはButtonの参照を保持していない
- ButtonはViewのComputed Property
var body: Self.Body { get }
で呼び出されているだけ
- ButtonはViewのComputed Property
- Buttonはactionクロージャをescapingはしている
- escapingしているがactionクロージャはContentViewの値をキャプチャしている(値がコピーされている)だけ
- そもそも
[weak self]
にできない - 構造体なので値がコピーされている
- おそらくアドレスを調べると別物となっているはず
- 基本的には構造体のメンバを変更することも出来ない(mutateにできない)
- おそらくアドレスを調べると別物となっているはず
- そもそも
つまり参照をしていないわけです。これを次のように図で概略を示します。
@ObservedObject
や@State
について
Viewの構造体が保持する@ObservedObject
や@State
は変更できる件
こっから話変わります。循環参照の話は終わりです。
「基本的には構造体のメンバを変更することも出来ない」というのを上で書きましたが、Buttonのactionクロージャで@ObservedObject
や@State
は変更できるはずです。
import SwiftUI
struct ContentView: View {
@State var state = false
var body: some View {
Button(action: {
self.state.toggle()
self.foo()
}) {
Text("ここを押しなさい...")
}
}
func foo() {
print("foo \(state)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ボタンを押すと次のようにstate
が変化します。
foo true
foo false
Viewの構造体が保持する@ObservedObject
や@State
が更新されても値が変わらない件
Viewが更新される際にstructのSubViewは作り直されています。次の記事が参考になるでしょう。
「SwiftUIのSubViewは画面更新ごとに生成と破壊を繰り返す」
https://qiita.com/yimajo/items/a0180e66f4f93120287e
これについては@ObservedObject
や@State
は構造体のメンバとなっていますが、これらは「別の領域に保持されている」か、もしくは「Viewの更新時にその値がうまいこと引き継がれる」のでしょう。
ここでリファレンスを読むと、@ObservedObject
や@State
はプロトコルDynamicPropertyに準拠しており、DynamicPropertyのupdate
メソッドはbodyの再計算時、つまりViewの更新前に値を再セットするような気がします。
Updates the underlying value of the stored value.
SwiftUIフレームワーク側の定義を読むとそんな感じの事も書いています
/// Represents a stored variable in a `View` type that is dynamically
/// updated from some external property of the view. These variables
/// will be given valid values immediately before `body()` is called.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol DynamicProperty {
/// Called immediately before the view's body() function is
/// executed, after updating the values of any dynamic properties
/// stored in `self`.
mutating func update()
}
@ObservedObject
や@State
などの変数は、Viewが更新されるbody()
呼び出しのタイミングでupdateメソッドが動作し、その際に値が再セットされることでうまいこと引き継がれるんでしょう。
まとめ
- SwiftUIのViewは構造体
- (ここに書いてなかったけどクラスにするとコンパイルエラー)
- クロージャにキャプチャするとコピーされているのでそもそも参照されていない
- View構造体メンバの
@ObservedObject
や@State
やらの値はどうやって元のままなの?-
DynamicProperty
の仕組みによってうまいこと引き継がれている- 1描画ごとにView構造体は都度作り直されることもあるし、そうでないこともある
-
- View構造体メンバの