Open2
[SwiftUI] AppStorageの変更がGeometryReaderより下に伝わらないバグ
実行環境
Xcode 15.0 (15A240d)
iPhone 15 Pro (17.0) Simulator
現象
struct Preview: View {
@AppStorage("count")
var count = 0
var body: some View {
let _ = print("body")
GeometryReader { reader in
let _ = print("under geometry")
Button {
count += 1
} label: {
Text(count.description)
.font(.largeTitle)
.frame(width: reader.size.width, height: reader.size.height)
}
}
}
}
countの表示が更新されない。printを入れてデバッグしてみると
そもそもprint("under geometry")
が(初回以降)呼ばれていないことがわかった。
この挙動は、GeometryReaderのEquatable
がtrueになっているのでは?と推測
参考: https://qiita.com/fuziki/items/6fe7a304b30146ba43c7
なおStateだと正常に動作する。意味不明
GeometryReaderの定義を見ると、クロージャーだけを持つViewっぽい
public var content: (SwiftUI.GeometryProxy) -> Content
これを模したRow
と言うViewを自作し、検証していく
↓検証コード
struct RowPreview: View {
@AppStorage("count")
var count = 0
var body: some View {
let _ = print("body")
Row {
let _ = print("under row")
Button {
count += 1
} label: {
Text(count.description)
}
}
}
}
クロージャー
struct Row<Content: View>: View {
let view: () -> Content
init(@ViewBuilder view: @escaping () -> Content) {
self.view = view
}
var body: some View {
let _ = print("row body")
view()
}
}
- 更新されない
- under row, row bodyが呼ばれない
- Stateだと呼ばれる
クロージャー+引数
struct Row<Content: View>: View {
let view: (Int) -> Content
init(@ViewBuilder view: @escaping (Int) -> Content) {
self.view = view
}
var body: some View {
let _ = print("row body")
view(Int.random(1...100)) // 毎回の呼び出しで引数が異なるように
}
}
多分これがGeometryReaderと同じ形
- 更新されない
- 引数が違うとかは関係なさそう
- まあそもそも"under row"すら呼ばれてないので当たり前か
クロージャー+他に変数
struct Row<Content: View>: View {
let count: Int
let view: (Int) -> Content
init(@ViewBuilder view: @escaping (Int) -> Content) {
self.view = view
self.count = Int.random(1...100)
}
var body: some View {
let _ = print("row body")
view(count)
}
}
- 更新される
- 変数のcountがEquatableに影響してfalseになっているから
Viewにする
struct Row<Content: View>: View {
let view: Content
init(@ViewBuilder view: @escaping () -> Content) {
self.view = view()
}
var body: some View {
view
}
}
- 更新される
- ViewならEquatableでfalseにしてくれるのか?
クロージャー + Equatable
struct Row<Content: View>: View, Equatable {
static func == (lhs: Row<Content>, rhs: Row<Content>) -> Bool {
false
}
let view: () -> Content
init(@ViewBuilder view: @escaping () -> Content) {
self.view = view
}
var body: some View {
let _ = print("row body")
view()
}
}
- 更新される
- Equatableを明示的にfalseにしているから
考察
- クロージャーだけを変数にもち、Equatableに準拠していないViewの場合、謎の判定基準によりEquatableがtrueになる。
- AppStorageだと起こるがStateだと起こらず、
GeometryReader
とかScrollViewReader
のSwiftUIのViewでもなるのは流石にAppStorageのバグでしょ
対処方法
- AppStorageを使わない
- 自作Viewだけの問題なら
Equatable
を実装して常にfalseにする