LoginSignup
30

SwiftUIのViewはなぜクロージャでselfを書いても循環参照しないのか

Last updated at Posted at 2020-04-19

SwiftUIでコードを書いていると、クロージャでViewのselfを頻繁に使うと思います。大抵のiOSアプリ開発者はselfを書かなければいけない場面にのみselfを利用しているので、循環参照しないようにweakもしくはunownedにすることを考慮したくなりますが、SwiftUIのViewはそこは考慮しないでも良いようです。この記事はその理由などを書いています。

なぜSwiftUIでselfが登場しても循環参照しないのか

本題です。SwiftUIでselfを頻繁に使う例として、Buttonのactionクロージャでselfを使う例を利用します。

画面としては次のようなボタンだけのContentViewを考えます。

スクリーンショット 2020-04-19 20.46.24.png
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はactionクロージャをescapingはしている
  • escapingしているがactionクロージャはContentViewの値をキャプチャしている(値がコピーされている)だけ
    • そもそも[weak self]にできない
    • 構造体なので値がコピーされている
      • おそらくアドレスを調べると別物となっているはず
        • 基本的には構造体のメンバを変更することも出来ない(mutateにできない)

つまり参照をしていないわけです。これを次のように図で概略を示します。

PNGイメージ.png

@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に準拠しており、DynamicPropertyupdateメソッドは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構造体は都度作り直されることもあるし、そうでないこともある

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30