iOS・AndroidSwift

【SwiftUI】.onAppear を用いて Viewがレンダリングされる順番とイベントの実行タイミングを追ってみる

iOS・Android
この記事は約16分で読めます。

.onAppear を用いて、SwiftUIにおけるViewのライフサイクルとイベント処理の挙動を確認することができます。

.onAppear

Viewが表示されるタイミングで1度だけ呼ばれるメソッドです。

onAppear(perform:) | Apple Developer Documentation
Adds an action to perform before this view appears.

以下のコードを実行するとわかりますが、クロージャーの処理が完了するまでビューの表示は実行されません。同期的に処理を行いたい時に使用するモディファイアです。

struct ContentView: View {
    var body: some View {
        Text("このビューはonAppear後に表示されます")
            .onAppear {
                print("処理を開始します")
                Thread.sleep(forTimeInterval: 5) // 5秒間スリープ
                print("処理が完了しました")
            }
    }
}

.onAppear を活用することで、SwiftUIのビューがレンダリングされる順番を捕捉することができます。

Viewのレンダリングを追う

以下コードを確認したいViewの中に設置することでレンダリングのタイミングを確認することができます。

ContentViewbodyが再評価されるたびにRefreshCheckerの新しいインスタンスが生成され、init(viewName: String)が呼び出されます。

struct RefreshChecker {
    var viewName: String = ""
    init(viewName: String) {
        print("🐣\(viewName)")
    }
}

// 使い方
struct ContentView: View {
    var body: some View {
        let checker = RefreshChecker(viewName: "ContentView🍓")
        Text("テキスト")
    }
}

ひとまずこの方法でレンダリングの捕捉を行うことができます。

親Viewと子Viewの場合

さて、以下のコードはどんな結果になるでしょうか。予想してみてください。

import SwiftUI

// 子Viewの定義
struct ChildView: View {
    @State var refreshCount = 0
    var body: some View {
        let refreshChecker = RefreshChecker(viewName: "子View \(refreshCount)")
        Text("子Viewがレンダリングされました")
            .onAppear {
                print("子ViewのonAppearが呼び出されました")
            }
    }
}

// 親Viewの定義
struct ParentView: View {
    @State var refreshCount = 0
    var body: some View {
        let refreshChecker = RefreshChecker(viewName: "親View \(refreshCount)")
        VStack {
            Text("親Viewがレンダリングされました")
                .onAppear {
                    print("親ViewのonAppearが呼び出されました")
                }
            ChildView() // 子Viewの呼び出し
        }
    }
}

答えは以下です。

🐣親View 0
🐣子View 0
子ViewのonAppearが呼び出されました
親ViewのonAppearが呼び出されました

先に親、ついで子Viewのインスタンス化が走っているのは、感覚的にも一致しますね。ParentViewのレンダリングが子Viewのレンダリングより先に行われています。

レンダリングの後にonAppearが走っている点から、SwiftUIはViewbodyをレンダリングした後、onAppearイベントをトリガーすることがわかります。

ChildViewonAppearParentViewonAppearより先にトリガーされる点から、子Viewのレンダリング処理が完了 => イベントハンドラが親Viewのレンダリング完了前に実行されることがわかります。

親Viewのレイアウトに含まれる全ての子Viewがレンダリングされ、そのイベント処理が完了した後に、ようやく親ViewのonAppearイベントハンドラがトリガーされます。

こちらを踏まえると、RefreshCheckerのインスタンス生成は、レイアウトプロセスの「提案」と「確定」の段階よりも前に行われることがわかります。

基本的なモディファイアとコンテナビュー

StackView (VStack, HStack, ZStack)

以下のコードはどんな結果になるでしょう。

import SwiftUI

struct ParentView: View {
    @State var refreshCount = 0
    var body: some View {
        let refreshChecker = RefreshChecker(viewName: "ParentView \(refreshCount)")
        VStack {
            Text("テキスト1")
                .onAppear {
                    print("テキスト1のonAppearが呼び出されました")
                }
            
            Text("テキスト2")
                .onAppear {
                    print("テキスト2のonAppearが呼び出されました")
                }
        }
        .onAppear {
            print("VStackのonAppearが呼び出されました")
        }
    }
}

答えは以下です。

🐣ParentView 0
VStackのonAppearが呼び出されました
テキスト2のonAppearが呼び出されました
テキスト1のonAppearが呼び出されました

外側にあるViewonAppearを先にトリガーするようです。

この例では、VStackonAppearが、その内部にあるTextビューのものより先に実行されます。

テキスト2のonAppearが先に実行されている点は少々直感に反していたのでびっくりしました。

Viewの性質により変動が発生するのか気になったので、試しに以下のコードを実行してみました。

        VStack {
            Image("恐竜")
                .onAppear {
                    print("恐竜1のonAppearが呼び出されました")
                }
            Text("テキスト1")
                .onAppear {
                    print("テキスト1のonAppearが呼び出されました")
                }
            
            Text("テキスト2")
                .onAppear {
                    print("テキスト2のonAppearが呼び出されました")
                }
            Image("恐竜")
                .onAppear {
                    print("恐竜2のonAppearが呼び出されました")
                }
        }

結果が以下です。


恐竜2のonAppearが呼び出されました
テキスト2のonAppearが呼び出されました
テキスト1のonAppearが呼び出されました
恐竜1のonAppearが呼び出されました

シンプルな構造の場合、内側のViewは下から順にイベントが実行されるようです。

また、Viewのタイプに応じた変動も、少なくともTextとImageについてはなさそうでした。

.background() モディファイア

以下のコードはどんな結果になるでしょう。

import SwiftUI

struct ParentView: View {
    @State var refreshCount = 0
    var body: some View {
        let refreshChecker = RefreshChecker(viewName: "ParentView \(refreshCount)")
        VStack {
            Image("恐竜")
                .background {
                    Image("恐竜")
                        .onAppear {
                            print("恐竜1のbackgroundのonAppearが呼び出されました")
                        }
                }
                .onAppear {
                    print("恐竜1のonAppearが呼び出されました")
                }
            
            Image("恐竜")
                .onAppear {
                    print("恐竜2のonAppearが呼び出されました")
                }
                .background {
                    Image("恐竜")
                        .onAppear {
                            print("恐竜2のbackgroundのonAppearが呼び出されました")
                        }
                }
        }
        .background {
            Color.red
                .onAppear {
                    print("VStackのbackgroundのonAppearが呼び出されました")
                }
        }
        .onAppear {
            print("VStackのonAppearが呼び出されました")
        }
    }
}

結果は以下です。

🐣ParentView 0
VStackのbackgroundのonAppearが呼び出されました
恐竜2のbackgroundのonAppearが呼び出されました
恐竜1のbackgroundのonAppearが呼び出されました
VStackのonAppearが呼び出されました
恐竜2のonAppearが呼び出されました
恐竜1のonAppearが呼び出されました

この結果は、VStackのonAppearbackgroundの順序を変えても変動しません。

レンダリング処理が先に実行されて、その後にViewのイベント処理が実行されると言うことが、明確にわかる結果ですね。

VStackbackgroundonAppearが最初にトリガーされる点から、VStackのレイアウトを計算する前に、backgroundに設定されたビューのレンダリングを先に実行している点が伺えます。

この結果から、background修飾子の使用は、ビュー階層のイベント処理の流れに直接影響を与えることがわかります。

.overlay() モディファイア

以下のコードを実行すると、どんな結果が起こるでしょう。

import SwiftUI

struct ParentView: View {
    @State var refreshCount = 0
    var body: some View {
        let refreshChecker = RefreshChecker(viewName: "ParentView \(refreshCount)")
        VStack {
            Image("恐竜")
                .background {
                    Image("恐竜")
                        .onAppear {
                            print("恐竜1のbackgroundのonAppearが呼び出されました")
                        }
                        .overlay {
                            Color.red
                                .onAppear {
                                    print("恐竜1のbackgroundのoverlayのonAppearが呼び出されました")
                                }
                        }
                }
                .onAppear {
                    print("恐竜1のonAppearが呼び出されました")
                }
                .overlay {
                    Color.red
                        .onAppear {
                            print("恐竜1のoverlayのonAppearが呼び出されました")
                        }
                }
            
            Image("恐竜")
                .onAppear {
                    print("恐竜2のonAppearが呼び出されました")
                }
                .overlay {
                    Color.red
                        .onAppear {
                            print("恐竜2のoverlayのonAppearが呼び出されました")
                        }
                    
                }
                .background {
                    Image("恐竜")
                        .onAppear {
                            print("恐竜2のbackgroundのonAppearが呼び出されました")
                        }
                        .overlay {
                            Color.red
                                .onAppear {
                                    print("恐竜2のbackgroundのoverlayのonAppearが呼び出されました")
                                }
                        }
                }
        }
        .onAppear {
            print("VStackのonAppearが呼び出されました")
        }
        .background {
            Color.red
                .onAppear {
                    print("VStackのbackgroundのonAppearが呼び出されました")
                }
                .overlay {
                    Color.red
                        .onAppear {
                            print("VStackのbackgroundのoverlayのonAppearが呼び出されました")
                        }
                }
        }
        .overlay {
            Color.red
                .onAppear {
                    print("VStackのoverlayのonAppearが呼び出されました")
                }
        }
    }
}

いよいよややこしくなってきた感じを受けますね。結果は以下です。

🐣ParentView 0
VStackのoverlayのonAppearが呼び出されました
VStackのbackgroundのoverlayのonAppearが呼び出されました
VStackのbackgroundのonAppearが呼び出されました
恐竜2のbackgroundのoverlayのonAppearが呼び出されました
恐竜2のbackgroundのonAppearが呼び出されました
恐竜2のoverlayのonAppearが呼び出されました
恐竜1のoverlayのonAppearが呼び出されました
恐竜1のbackgroundのoverlayのonAppearが呼び出されました
恐竜1のbackgroundのonAppearが呼び出されました
VStackのonAppearが呼び出されました
恐竜2のonAppearが呼び出されました
恐竜1のonAppearが呼び出されました

最初にVStackのoverlayが呼び出されています。

overlay修飾子の順番は、Viewによって若干変動させましたが、結果には明らかな規則性があることが見て取れます。

基本的に、下から上へモデファイアのレンダリングは実行されるようです。<code> background内については、overlayが先に実行されているようですね。

まとめ

本記事では.onAppear を用いて Viewがレンダリングされる順番とイベントの実行タイミングを追いました。

総括すると、以下の発見がありました。

  • レンダリング処理が先に実行されて、その後にViewのイベント処理が実行される
    Viewbodyをレンダリングした後、onAppearイベントをトリガーする
  • より下位の層にあるViewからイベント処理が実行される
    子ViewのonAppearイベント => 親ViewのonAppearイベント
  • 同階層のViewについては、外側にあるViewonAppearを先にトリガーする
  • backgroundモディファイアに設定されたビューのonAppearを、付与されたViewのonAppearに先んじて実行している
  • モデファイアのレンダリングは下から上へ実行される

本稿では触れませんでしたが、ListやForEach、GeometryReader、TabViewの挙動も確認すると、面白い発見があるかもしれません。また気合いが持ったら試してみたいです。