SwiftUIのLazyVStackの孫ViewのStateが画面外でリセットされる
とても長いタイトルですが、タイトルままです
他の方が報告されている、こちらのOpen Radarと同じ問題です
iOS 17.2.1、Xcode 15で確認してます
起きている現象
例えば下記のようにLazyVStackの子ViewとしてChildView、ChildViewの子ViewとしてSubViewを配置します
(つまりLazyVStackから見たら、SubViewは孫View)
この時、SubViewの持つ@State及び@StateObjectが、そのSubViewが画面外に行った際にリセットされてしまいます
ChildViewの方は値が保持されているので、おそらく不具合かなと考えています

struct ContentView: View {
@State var items: [String] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map { String($0) }
var body: some View {
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { ChildView(item: $0) }
}
}
}
}
// 子View
struct ChildView: View {
let item: String
@State var flag = false
var body: some View {
HStack {
Toggle("\(item): ChildView", isOn: $flag)
.fixedSize()
SubView()
}
.padding()
.border(Color.blue)
}
}
// 孫View
struct SubView: View {
@State var flag = false
var body: some View {
Toggle("SubView", isOn: $flag)
.fixedSize()
.padding()
.border(Color.blue)
}
}
対策
ChildViewで値を持ち、SubViewにBindingで渡すのがシンプルな解決策だと思います
struct ChildView: View {
let item: String
@State var flag = false
+ @State var flagForSubView = false
var body: some View {
HStack {
Toggle("\(item): ChildView", isOn: $flag)
.fixedSize()
+ SubView(flag: $flagForSubView)
- SubView()
}
.padding()
.border(Color.blue)
}
}
struct SubView: View {
+ @Binding var flag: Bool
- @State var flag = false
var body: some View {
Toggle("SubView", isOn: $flag)
.fixedSize()
.padding()
.border(Color.blue)
}
}
しかし、SubViewを共通化しライブラリとして切り出している場合、親ViewにBindingを要求するのは若干、ライブラリとしての使い勝手が悪くなりがちです
そこで、ワークアラウンド(と言っていいか怪しいレベル)ですが、@State、@StateObjectを付けなければ値は保持されるので、attributeアリとナシのプロパティそれぞれで値を保持し、View内で適切に復元するロジックを組む事で、一応状態を維持できます
画像キャッシュライブラリのKingfisherのissueの議論がとても参考になりました
終わりに
遭遇したSwiftUIのLazyVStackの不具合についての話でした
もし、より良い解決手段をご存知でしたら教えて頂けるととても嬉しいです!