@State
#SwiftUI
@State は、SwiftUI によって読み書きされる値であることを示す property wrapper。
役割
@State はどのような時に使うか?というと、よく以下のような説明がされる。
その View 自体が source of truch となる場合には、@State を利用してプロパティを定義する
その View ではなく他の View が source of truth となる場合には、@Binding を利用してプロパティを定義する
WWDC のビデオでも「source of truch を表現したい場合に @State を使う」といったようなことが解説されている。
There isn't some parent view that can pass it in, so we need to establish a local source of truth. The simplest source of truth in SwiftUI is State.
Data Essentials in SwiftUI - WWDC 2020
ドキュメントでも同様。
Use state as the single source of truth for a given value type that you store in a view hierarchy.
https://developer.apple.com/documentation/swiftui/state
@State の具体的な役割は、
SwiftUI によって View の Identity (SwiftUI) に紐付けて保持される
Binding (@Binding) を利用して、他の View と値を同期できる
値を読み書きできるようになる
スレッドセーフ
あたりかと思われる。
これらの内「値を読み書きできるようになる」ということ?となるかもしれなけど、実は View の body 内では自身のプロパティを書き換えられない。下記のようなエラーとなってしまう。
code:swift
import PlaygroundSupport
import SwiftUI
struct CountView: View {
var counter: Int = 0
var body: some View {
HStack {
Button("Press Me") { counter += 1 } // <- Error: Left side of mutating operator isn't mutable: 'self' is immutable
Text("\(counter)")
}
}
}
PlaygroundPage.current.setLiveView(CountView())
この例の場合、@State を counter プロパティに付与することで、値が読み書きできるようになる。
参照型のプロパティの監視
下記のような Count struct を用意して、それを @State として定義すると、ボタンを押すたびに View の表示が更新される。
code:swift
import PlaygroundSupport
import SwiftUI
struct Count {
var counter: Int = 0
}
struct CountView: View {
@State var count: Count = .init()
var body: some View {
HStack {
Button("Press Me") { count.counter += 1 }
Text("\(count.counter)")
}
}
}
PlaygroundPage.current.setLiveView(CountView())
しかし、これを struct から class に変えると、動かなくなってしまう。何度ボタンを押しても、View にカウントが反映されない。
code:swift
class Count {
var conter: Int = 0
}
@State はプロパティの値が変更された時にそれを View に通知する。値型の場合は @State でその変更を検知できるけど、参照型の場合は参照型自体が置き換えられないとそれを検知できない。
However the view will only update when the reference to the object changes, such as when setting the property with a reference to another object.
https://developer.apple.com/documentation/swiftui/state
上記の例だと、counter プロパティは実際には変更されているのだけど、その変更が通知できていないので、変更が View に反映されない。例えば、以下のように実装し、何度か Press Me を押下した後 Refresh を押下して View を強制的に再描画すると、counter の値が反映されるので、裏では更新できていたことがわかる。
code:swift
import PlaygroundSupport
import SwiftUI
class Count {
var counter: Int = 0
}
struct CountView: View {
@State var count: Count = .init()
@State var refreshID = UUID()
var body: some View {
VStack {
HStack {
Button("Press Me") { count.counter += 1 }
Text("\(count.counter)")
}
Button("Refresh") { refreshID = UUID() }
}
.id(refreshID)
}
}
PlaygroundPage.current.setLiveView(CountView())
参照型のプロパティを監視する方法は、現状以下の方法がある。
参照型を ObservableObject に適合させ、通知すべきプロパティを @Published でマークし、@StateObject や @EnvironmentObject プロパティとして参照する
Observable マクロを使用する
Using @State with classes - a free Hacking with iOS