背景
SwiftUIのViewはView Identityと呼ばれる識別子によって管理されています。このView Identityが変化すると完全に別のViewであると判断されるため、再描画コストがかかり、内部に持つ状態(@State
)が初期化され、アニメーションなども期待通り動作しなくなります。
以上から、意図しないView Identityの変化はバグを生むため、避けなければいけません。
参考
本題
基本的にはコードからView Identityが変化する可能性に気付いて修正するのが理想ですが、既存のViewを調査する場合にコードを全て読み解いてバグの有無を判断するのはめんどくさい場合もあります。そんな場合に役立つmodifier。
private struct HighlightViewIdentityChanged: ViewModifier {
@State private var loaded = false
func body(content: Content) -> some View {
content
.overlay {
if !loaded {
Color.yellow.opacity(0.5)
}
}
.onAppear {
withAnimation {
loaded = true
}
}
}
}
public extension View {
func _highlightViewIdentityChanged() -> some View {
modifier(HighlightViewIdentityChanged())
}
}
View Identityが変わるとStateが初期化される挙動を利用して、View Identityが変わった(生成された)瞬間Viewを黄色にハイライトする、というアプローチ。
使い方例
ViewIdentityが意図せず変わっている疑惑があるView(if, switchの内側のView)や、変わると困るView(描画コストが高いListとか、状態を内部に持ったView)に対して._highlightViewIdentityChanged()
を付けてアプリをデバッグしてみる。
基本的にはmodifierの中で最も内側につけるのを推奨。
struct ContentView: View {
@State var condition = false
var body: some View {
List {
Text("Hello")
Button("toggle") {
condition.toggle()
}
}
._highlightViewIdentityChanged()
.if(condition) {
$0.foregroundStyle(.blue)
}
}
}
extension View {
@ViewBuilder
func `if`<Content: View>(
_ condition: Bool,
@ViewBuilder transform: (Self) -> Content
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
ListのView Identityが変わっていることが視覚的にわかる。
なお、このmodifierは付けた一点のView Identityの変化のみ検知する。つまり付けたViewの子ViewのView Identityだけが変化していても検知しない。
例えばText("Hello")
のView Identityのみ変わるケースも検知したい場合はまた別の._highlightViewIdentityChanged()
をつける必要がある。
struct ContentView: View {
@State var condition = false
var body: some View {
VStack {
Text("Hello")
._highlightViewIdentityChanged()
.if(condition) {
$0.foregroundStyle(.blue)
}
Button("toggle") {
condition.toggle()
}
}
// これはText("Hello")のView Identity変化を検知しない
._highlightViewIdentityChanged()
}
}