【SwiftUI】LazyVStackのonAppearについて調査した

こんにちは。[ymurao2](https://twitter.com/ymurao2)です！
`ScrollView` + `LazyVStack`で縦に長い画面を実装しているとき、`VStack`と`LazyVStack`の使い分けについて悩むことが少なからずあったので、調査しました

:::note info
動作環境
Xcode15.0.1
:::
以下のような画面で調査しました
![CleanShot 2023-12-17 at 14.40.14.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/4cb0fd54-5ce3-ae81-1325-9f63ddab7a57.gif)

調査コード
```diff_swift
ScrollView {
+   // ここと
    LazyVStack {
+       // ここと
        LazyVStack {
            ForEach(0..<25, id: \.self) { i in
                Text(String(i))
                    .padding()
                    .onAppear {
                        print("upper:" + String(i))
                    }
            }
        }
        .background(Color.gray)
+       // ここと
        LazyVStack {
            ForEach(25..<50, id: \.self) { i in
                Text(String(i))
                    .padding()
                    .onAppear {
                        print("middle:" + String(i))
                    }
            }
        }
        .background(Color.red)
+       // ここをいじります
        LazyVStack {
            ForEach(50..<75, id: \.self) { i in
                Text(String(i))
                    .padding()
                    .onAppear {
                        print("lower:" + String(i))
                    }
            }
        }
        .background(Color.blue)
    }
}
```


## 1.`LavyVStack` + `LazyVStack`

```swift
ScrollView {
    LazyVStack {
        LazyVStack {
            // 省略
        }
        .background(Color.gray)
        LazyVStack {
            // 省略
        }
        .background(Color.red)
        LazyVStack {
            // 省略
        }
        .background(Color.blue)
    }
}
```
![CleanShot 2023-12-17 at 14.50.41.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/d2057636-fbb4-c855-1dff-b6833fec2d59.gif)

:::note alert
`View`が表示される前に`onAppear`は発火しているので、ログは実際に表示されるタイミングより早く表示されています
[Stack Overflow参照](https://stackoverflow.com/questions/70147827/lazyvstack-row-onappear-is-called-early)
:::

このパターンの場合、全てのViewが遅延読み込みされていました

## 2.`LazyVStack` + `VStack`
```diff_swift
// 最初のコードとのdiff
ScrollView {
    LazyVStack {
+       VStack {
            // 省略
        }
        .background(Color.gray)
+       VStack {
            // 省略
        }
        .background(Color.red)
+       VStack {
            // 省略
        }
        .background(Color.blue)
    }
}
```
![CleanShot 2023-12-17 at 15.04.15.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/dc55d415-7786-58df-3b82-cdd353049330.gif)

このパターンの場合、`VStack`の先頭がレンダリングされるときに、`VStack`の内の`onAppear`が**全て**発火していました

## 3.`VStack` + `LazyVStack`

```diff_swift
// 最初のコードとのdiff
ScrollView {
+   VStack {
        LazyVStack {
            // 省略
        }
        .background(Color.gray)
        LazyVStack {
            // 省略
        }
        .background(Color.red)
        LazyVStack {
            // 省略
        }
        .background(Color.blue)
    }
}
```
![CleanShot 2023-12-17 at 15.10.03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/418de345-3fb1-aabf-9131-c0a80038b609.gif)

1.の`LavyVStack + LazyVStack`と同じ結果になりました

:::note alert
このパターンも良さそうに見えましたが、別の問題がありました
:::
```diff_swift
// 最初のコードとのdiff
ScrollView {
    VStack {
        LazyVStack {
            // 省略
        }
        .background(Color.gray)
        LazyVStack {
            // 省略
        }
        .background(Color.red)
        LazyVStack {
            // 省略
        }
        .background(Color.blue)
+       Text("表示されたぞ！！！")
+           .onAppear {
+               print("表示されたぞ！！！")
+           }
    }
}
```
![CleanShot 2023-12-17 at 15.25.40.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/ac7783a5-acbf-5dff-1221-30553ded0532.gif)
一番下に`Text`を差し込んだところ、こちらは表示される前に`onAppear`が発火しました
これは期待通りの動作とは言えなさそうです

## 4.`VStack` + `VStack`
```diff_swift
// 最初のコードとのdiff
ScrollView {
+   VStack {
+       VStack {
            // 省略
        }
        .background(Color.gray)
+       VStack {
            // 省略
        }
        .background(Color.red)
+       VStack {
            // 省略
        }
        .background(Color.blue)
    }
}
```
![CleanShot 2023-12-17 at 15.12.44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/68a5b54f-4963-653d-9721-96b91edae173.gif)
Gifだと分かりにくいですが、**全ての`View`の`onAppear`が一度に発火**していました

# まとめ
| 実装方法 | 結果 | 評価 |
| --- | --- | --- |
| `LavyVStack` + `LazyVStack` | 全ての`View`が遅延読み込みされた | ◎ |
| `LazyVStack` + `VStack` | `VStack`内の`View`の`onAppear`が同時に発火 | × |
| `LazyVStack` + `VStack` | `VStack`内の要素が全て`VStack`に入っていれば問題なさそう | △ |
| `VStack` + `VStack` | 全ての`View`が遅延読み込みされない | × |

調査の結果、`LazyVStack` + `LazyVStack`で実装するのが良さそうに見えました

:::note info
この記事がお役に立てば、ぜひいいね👍お願いします！
:::


# 参考文献
[LazyVStackを使用すると描画処理が何度も走ってしまう](https://zenn.dev/ueshun/articles/a687c470c7b903)
[lazyvstack-row-onappear-is-called-early](https://stackoverflow.com/questions/70147827/lazyvstack-row-onappear-is-called-early)