View.onAppear(perform:)が呼ばれないことがある

SwiftUIの View のライフサイクルで少しハマったのでメモしておきます。

  • 環境:

  • 要件:

    • ある画面(View)が表示されたタイミングで何か処理を実行したい

UIKitの場合にはViewControllerの viewWillAppear / viewDidAppear なんかで実行することになりますが、
SwiftUIのViewの場合は onAppear(action:) がそれに相当するかと思います。

ですが、このonAppear(action:)が自分の想定していたタイミングでは呼ばれないということがありましたので紹介します。

例として、次のような単純な親子関係のViewのがあり、その子Viewが表示された時にある処理を実行したいとします。

Parent
  - Child  <== このViewが表示されたらonAppearで処理を実行したい!!

コードだとこうなります。

// 子View
struct Child: View {
    init() { print("Child.init()") }
    var body: some View {
        Text("Child")
            // 子Viewが表示されたら処理を実行したい!!
            .onAppear { print("Child.onAppear(perform:)") }
    }
}

// 親View
struct Parent: View {
    init() { print("Parent.init()") }
    var body: some View {
        VStack {
            Child()
        }
    }
}

そしてこの親View( Parent )を表示すると次のようにログ表示されます。

Parent.init()
Child.init()
Child.onAppear(perform:) // ちゃんと呼ばれてる

ここまでは単純ですね。

問題は次のように 親Viewが状態を持つ場合 です。
親Viewの状態が変更されると、その子Viewは再構築されるのですが、 そのタイミングでは onAppear(perfom:) が呼ばれません。(これが想定と違ってた!!)

// 親View
struct Parent: View {
    @State private var myState = false // なにかの状態を追加
    
    init() { print("Parent.init()") }
    var body: some View {
        VStack {
            Child()
            
            // このボタンをタップするとParentの状態が変更されViewの再構築が発生する
            Button(action: {
                self.myState.toggle()
            }) {
                Text("myState = \(self.myState ? "true" : "false")")
            }
        }
    }
}

ここで最初に親View( Parent )を表示すると先ほどと同じログになるのですが、
ボタンをタップして親Viewの状態が変更されると、

Child.init()

となり、子VIew ( Child )の再構築が発生( init() が呼ばれる)するのですが、 onAppear(perfom:) が呼ばれないことがわかります。

なにが困るか?

当初は「ある画面を複数のViewに分割(コンポーネント化)し、そのViewに関連する初期化処理(データの取得など)は onAppear(perform:) で実行する」としたかったのですが、それだと通用しないことがわかりました。

では、常に呼ばれてる init() で初期化処理を行えば?となるかもしれませんが、 init() は表示する必要のないViewに対しても呼ばれるため、無駄な処理が発生してしまいます。

// 例えばこんな場合、すべての子Viewでinit()が呼ばれてしまう
NavigationView {
    List {
        ForEach(0..<100, id: \.self) { value in
            NavigationLink(destination: Child()) {  // すべての子Viewでinit()が呼ばれる
                Text("value: \(value)")
            }
        }
    }
}

このような挙動を考えると、 局所的なView状態以外のオブジェクト(モデル)のライフサイクルは完全にViewと切り離して管理するのがよさそうです。