.onAppear を用いて、SwiftUIにおけるViewのライフサイクルとイベント処理の挙動を確認することができます。
.onAppear
Viewが表示されるタイミングで1度だけ呼ばれるメソッドです。
以下のコードを実行するとわかりますが、クロージャーの処理が完了するまでビューの表示は実行されません。同期的に処理を行いたい時に使用するモディファイアです。
struct ContentView: View {
var body: some View {
Text("このビューはonAppear後に表示されます")
.onAppear {
print("処理を開始します")
Thread.sleep(forTimeInterval: 5) // 5秒間スリープ
print("処理が完了しました")
}
}
}
.onAppear を活用することで、SwiftUIのビューがレンダリングされる順番を捕捉することができます。
Viewのレンダリングを追う
以下コードを確認したいViewの中に設置することでレンダリングのタイミングを確認することができます。
ContentView
のbody
が再評価されるたびに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はView
のbody
をレンダリングした後、onAppear
イベントをトリガーすることがわかります。
ChildView
のonAppear
がParentView
のonAppear
より先にトリガーされる点から、子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が呼び出されました
外側にあるView
のonAppear
を先にトリガーするようです。
この例では、VStack
のonAppear
が、その内部にある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のonAppear
とbackground
の順序を変えても変動しません。
レンダリング処理が先に実行されて、その後にViewのイベント処理が実行されると言うことが、明確にわかる結果ですね。
VStack
のbackground
のonAppear
が最初にトリガーされる点から、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のイベント処理が実行される
View
のbody
をレンダリングした後、onAppear
イベントをトリガーする - より下位の層にあるViewからイベント処理が実行される
子ViewのonAppear
イベント => 親ViewのonAppear
イベント - 同階層のViewについては、外側にある
View
のonAppear
を先にトリガーする background
モディファイアに設定されたビューのonAppear
を、付与されたViewのonAppear
に先んじて実行している- モデファイアのレンダリングは下から上へ実行される
本稿では触れませんでしたが、ListやForEach、GeometryReader、TabViewの挙動も確認すると、面白い発見があるかもしれません。また気合いが持ったら試してみたいです。