状態管理でよく使われるアノテーションは以下の通りです。
@State
@Binding
@StateObject
@ObservedObject
@EnvironmentObject
Viewに状態を持たせたい
@State
をつけると、状態としてViewから変更可能になります。
変更されると即時Viewに反映されます。
struct ContentView: View {
@State var count = 0
var body: some View {
VStack {
Text("Count is \(count)")
Button("Count up") {
count += 1
}
}
}
}
子Viewに状態を渡したい
子Viewに渡すだけであれば、単純な引数として渡せばOKです。
親のStateが変更されると、子にそのまま伝わり、即時Viewに反映されます。
struct ContentView: View {
@State var count = 0
var body: some View {
VStack {
CountDisplay(count: count) // 渡す側
Button("Count up") {
count += 1
}
}
}
}
struct CountDisplay: View {
var count: Int // 渡される側
var body: some View {
Text("Count is \(count)")
}
}
親Viewに状態の変更を伝えたい
子Viewで状態が変わったことを親Viewでも検知したい場合は、@Binding
を使用します。
親Viewで@State
を管理し、子Viewにバインドするというイメージです。
struct ContentView: View {
@State var count = 0
var body: some View {
VStack {
CountDisplay(count: count)
CountButton(count: $count) // '$'をつけてバインドする
}
}
}
struct CountButton: View {
@Binding var count: Int // 変更が親に伝わる
var body: some View {
Button("Count up") {
count += 1
}
}
}
カスタムオブジェクトを扱いたい場合
SwiftUIでは原則struct
を使用することが推奨されています。
カスタムオブジェクトを実装する場合は可能な限りIntやStringなどのプリミティブ型で構成した構造体として実装します。
struct Person {
var name: String
var age: Int
var isPremium: Bool
}
上記のような構造体は、これまでに説明した@State
や@Binding
を使用してView内での状態管理に含めることができます。
クラスを扱いたい場合
何らかの理由でオブジェクトの定義をclass
として行う場合、class
は参照型であるため、SwiftUIはプロパティの変更を検出することができません。
class
を状態として管理したい場合は、そのclassをObservableObject
に準拠させる必要があります。
さらに、SwiftUIで変更を検出してほしいプロパティに@Published
を付与します。
class Person: ObservableObject {
@Published var name = ""
@Published var age = 0
@Published var isPremium = false
}
Viewで利用する際も、@State
は使用せず、@StateObject
を使用します。
struct ContentView: View {
@StateObject var person = Person()
var body: some View {
HStack {
Text(person.name)
Text(person.age.description)
Spacer()
Text(person.isPremium ? "P" : "N")
}
}
}
また、子Viewに対しクラスのまま渡してしまった場合、子Viewはクラスの変更を検出できません。(即時反映されない)
あくまで@StateObject
を付与したViewでのみ、変更がPublishされるのです。
これを回避するためには、子View側でも@ObservedObject
を付与して、監視対象であることを明示してください。
struct ContentView: View {
@StateObject var person = Person()
var body: some View {
PersonRow(person: person)
}
}
struct PersonRow: View {
var person: Person // これはダメ
@ObservedObject var person: Person // これでOK
var body: some View {
HStack {
Text(person.name)
Text(person.age.description)
Spacer()
Text(person.isPremium ? "P" : "N")
}
}
}
オプショナル型を扱いたい場合
ラップされる値がstruct
の場合、これまでのstruct
型と同様に@State
を使用して状態管理することが可能です。
しかし、class
の場合、さらに注意が必要となります。
まずオプショナル型は、歴とした一つの型であると考えてください。Person?
は、Person型に?がついただけだと考えるのではなく、Personをラップしただけのオプショナル型です。
オプショナル型は、struct
と同様に@State
を使用して状態管理できますが、その代わり@StateObject
や@ObservedObject
を付与することはできません。
また、この時SwiftUIが検知するのはPerson
の変更ではなくオプショナル型としての変更であることに注意してください。
つまり、Person?
の値がnil
であるか、Person
であるかの2択が変更された時にしか、Viewへの反映は行われないということです。Personの内容が変更されたことを反映するには、アンラップしたPersonクラスを状態管理する子Viewを作る必要があります。
struct ContentView: View {
@State var person: Person?
var body: some View {
if let person { // アンラップする
PersonRow(person: person)
}
}
}
struct PersonRow: View {
@ObservedObject var person: Person // アンラップされたものを監視する
var body: some View {
HStack {
Text(person.name)
Text(person.age.description)
Spacer()
Text(person.isPremium ? "P" : "N")
}
}
}
配列を扱いたい場合
配列の場合も、中身がstruct
の場合は問題ありませんが、class
の配列の場合は注意が必要です。
配列自体は、struct
と同様に@State
を使用して状態管理できますが、こちらも@StateObject
や@ObservedObject
を付与することはできません。
例えば[Person]
を定義したとき、SwiftUIが検知するのは配列としての変更であり、配列内の要素であるPerson
が変更されたとしてもViewに反映されることはありません。
配列の変更というのは、要素が追加された時、要素が削除された時、要素そのもの(IDなど)が完全に置き換わった時、順番が入れ替わった時などです。要素の内容変更をViewに反映したい場合は、要素ごとのPersonクラスを状態管理する子Viewを作る必要があります。
class Person: ObservableObject, Hashable {
@Published var name = ""
@Published var age = 0
@Published var isPremium = false
// 以下はHashableに準拠させるため(ForEachのため)に必要
let id = UUID()
func hash(into hasher: inout Hasher) {}
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.id == rhs.id
}
}
struct ContentView: View {
@State var persons: [Person] = []
var body: some View {
VStack {
ForEach(persons, id: \.self) { person in
// 要素を取り出す
PersonRow(person: person)
}
}
}
}
struct PersonRow: View {
@ObservedObject var person: Person // 取り出されたものを監視する
var body: some View {
HStack {
Text(person.name)
Text(person.age.description)
Spacer()
Text(person.isPremium ? "P" : "N")
}
}
}
グローバルなオブジェクトとして状態管理したい場合
宣言型UIの状態管理は、基本的にバケツリレーとなります。
つまり、親Viewから子View、孫Viewにかけて、必要なデータを渡し続けます。
しかしこのバケツリレーがあまりにも多かったり、中間階層のViewで全く使用しないオブジェクトは、いちいちViewの引数として定義したくない場合があります。
そんな時は@EnvironmentObject
を使用します。
なお、@EnvironmentObject
が付与できるのは、@ObservableObject
のみであるため、別途クラスの作成が必要です。
class MySettings: ObservableObject {
@Published var isUseFaceID = false
}
また、実際に使用するViewよりも上の階層で、インスタンスをセットしておく必要があります。一般的にはエントリーポイントである〇〇Appのファイルで記述されると思います。
@main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
+ .environmentObject(MySettings())
}
}
}
struct ContentView: View {
@StateObject var person = Person() // バケツリレーの場合
var body: some View {
ChildView(person: person)
}
}
struct ChildView: View {
var person: Person // 使わないけど渡される
var body: some View {
ChildChildView(person: person)
}
}
struct ChildChildView: View {
var person: Person // 使わないけど渡される
var body: some View {
ChildChildChildView(person: person)
}
}
struct ChildChildChildView: View {
@ObservedObject var person: Person // ここでやっと使う
@EnvironmentObject var settings: MySettings // 使うとこで呼べる
var body: some View {
VStack {
Text("Your name is \(person.name)")
Toggle("Use Face ID", isOn: $settings.isUseFaceID)
}
}
}
まとめ
とりあえず以上となります。他にも書くことがあれば追記します。
ご意見や修正案、ネタリクエストなどお気軽にコメントください!