6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIで状態管理したい時のチートシート

Last updated at Posted at 2025-01-13

状態管理でよく使われるアノテーションは以下の通りです。

  • @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のみであるため、別途クラスの作成が必要です。

MySettings.swift
class MySettings: ObservableObject {
    @Published var isUseFaceID = false
}

また、実際に使用するViewよりも上の階層で、インスタンスをセットしておく必要があります。一般的にはエントリーポイントである〇〇Appのファイルで記述されると思います。

SampleApp.swift
@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
+               .environmentObject(MySettings())
        }
    }
}
ContentView.swift
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)
        }
    }
}

まとめ

とりあえず以上となります。他にも書くことがあれば追記します。
ご意見や修正案、ネタリクエストなどお気軽にコメントください!

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?