[SwiftUI] iOS 16までのObservableObject, @ObservedObject, @StateObject。iOS 17からの@Observable

[SwiftUI] iOS 16までのObservableObject, @ObservedObject, @StateObject。iOS 17からの@Observable

Clock Icon2024.10.31

こんにちは。きんくまです。
今回はモデルの違いについてです。

iOS 16までのObservableObject, @ObservedObject, @StateObject

つくったもの

https://www.youtube.com/shorts/YjXHjU9L45w

ソースコード

class Counter: ObservableObject {
    @Published var count: Int = 0
    @Published var message: String = ""
    @Published var isOptionOn: Bool = true
}

struct CounterView: View {
    @ObservedObject var counter = Counter()

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)
            Toggle(isOn: $counter.isOptionOn) {
                Text("Option")
            }
            .padding(.bottom, 15)
            Text("message: \(counter.message)")
            Button {
                counter.message = "Hello"
            } label: {
                Text("set message")
            }
        }
    }
}

struct CounterView2: View {
    @StateObject private var counter = Counter()

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)
            Toggle(isOn: $counter.isOptionOn) {
                Text("Option")
            }
            .padding(.bottom, 15)
            Text("message: \(counter.message)")
            Button {
                counter.message = "Hello"
            } label: {
                Text("set message")
            }
        }
    }
}

struct ContentView: View {
    @State private var isOnInContentView: Bool = true

    var body: some View {
        VStack {
            Toggle(isOn: $isOnInContentView) {
                Text("Content View Toggle")
            }

            Text("ObservedObject")
                .padding(.top, 30)
            CounterView()
                .padding(10)
                .border(Color.blue)

            Text("StateObject")
                .padding(.top, 30)
            CounterView2()
                .padding(10)
                .border(Color.red)

        }
        .frame(width: 300)
    }
}

説明

241031-ios16-observableobject

構造は以下です。

  • ContentView
    |- CounterView -> @ObservedObjectで設定
    |- CounterView2 -> @StateObjectで設定

プログラムの書き方として、ObservableObjectで作ったモデルをView側で使うときは、上記のように@ObservedObjectや@StateObjectでセットすれば良いです。

それでここから本題で、動画をみていただくとわかりますが、挙動は以下です。

  1. ContentViewのToggleを変更
  2. ContentViewの再描画が走る
  3. CounterView(@ObservedObjectで設定)は、中身がリセットされるが、CounterView2(@StateObjectで設定)は中身がリセットされない

@ObservedObjectで設定したCounterViewが毎回リセットされるのは困ります。
なので、以下のように変更します。

CounterViewの中でCounter()を初期化するのではなく、親であるContentViewで初期化したものをプロパティとしてCounterViewに渡す

変更後のコード

class Counter: ObservableObject {
    @Published var count: Int = 0
    @Published var message: String = ""
    @Published var isOptionOn: Bool = true
}

struct CounterView: View {
    //@ObservedObject var counter = Counter()
    // ここを変更
    @ObservedObject var counter: Counter

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)
            Toggle(isOn: $counter.isOptionOn) {
                Text("Option")
            }
            .padding(.bottom, 15)
            Text("message: \(counter.message)")
            Button {
                counter.message = "Hello"
            } label: {
                Text("set message")
            }
        }
    }
}

struct CounterView2: View {
    @StateObject private var counter = Counter()

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)
            Toggle(isOn: $counter.isOptionOn) {
                Text("Option")
            }
            .padding(.bottom, 15)
            Text("message: \(counter.message)")
            Button {
                counter.message = "Hello"
            } label: {
                Text("set message")
            }
        }
    }
}

struct ContentView: View {
    @State private var isOnInContentView: Bool = true
    @State private var counter = Counter() // ここを追加

    var body: some View {
        VStack {
            Toggle(isOn: $isOnInContentView) {
                Text("Content View Toggle")
            }

            Text("ObservedObject")
                .padding(.top, 30)
            CounterView(counter: counter) // ここを変更
                .padding(10)
                .border(Color.blue)

            Text("StateObject")
                .padding(.top, 30)
            CounterView2()
                .padding(10)
                .border(Color.red)

        }
        .frame(width: 300)
    }
}

変更後の動画

これで親のViewの再描画があっても子供のViewにまで影響が出ませんでした

https://youtube.com/shorts/FUNv5ZUXzVk

まとめ

  • ObservableObjectで作ったモデルは @ObservedObject, @StateObject などでViewにセットする
  • @ObservedObjectは親からプロパティでもらう
  • @StateObjectは自分自身で初期化して持つ

iOS 17からの@Observable

つづいてiOS 17からの@Observableです。

つくったもの

挙動はiOS 16の修正版と同じです

https://youtube.com/shorts/uCjWyzGvS-M

ソースコード

@Observable class Counter {
    var count: Int = 0
    var message: String = ""
    var isOptionOn: Bool = true
}

struct CounterView: View {
    // @ObservedObjectをつけない
    var counter: Counter

    var body: some View {
        // $counterを使いたいので@Bindableで宣言
        // そうでなければこの記述は不要
        @Bindable var counter = counter

        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)
            Toggle(isOn: $counter.isOptionOn) {
                Text("Option")
            }
            .padding(.bottom, 15)
            Text("message: \(counter.message)")
            Button {
                counter.message = "Hello"
            } label: {
                Text("set message")
            }
        }
    }
}

struct CounterView2: View {
    // @StateObjectじゃない
    @State private var counter = Counter()

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)
            Toggle(isOn: $counter.isOptionOn) {
                Text("Option")
            }
            .padding(.bottom, 15)
            Text("message: \(counter.message)")
            Button {
                counter.message = "Hello"
            } label: {
                Text("set message")
            }
        }
    }
}

struct ContentView: View {
    @State private var isOnInContentView: Bool = true
    @State private var counter = Counter()

    var body: some View {
        VStack {
            Toggle(isOn: $isOnInContentView) {
                Text("Content View Toggle")
            }

            Text("ObservedObject")
                .padding(.top, 30)
            CounterView(counter: counter)
                .padding(10)
                .border(Color.blue)

            Text("StateObject")
                .padding(.top, 30)
            CounterView2()
                .padding(10)
                .border(Color.red)

        }
        .frame(width: 300)
    }
}

説明

少しずつ書き方が違っています。

(旧)iOS 16までのモデル

class Counter: ObservableObject {
    @Published var count: Int = 0
    @Published var message: String = ""
    @Published var isOptionOn: Bool = true
}

(新)iOS 17からのObservationを使ったモデルの書き方

  • class定義の前に@Observableがつく
  • プロパティに@Publishedをつけない
@Observable class Counter {
    var count: Int = 0
    var message: String = ""
    var isOptionOn: Bool = true
}

(旧)iOS 16までの@ObservedObject

struct CounterView: View {
    @ObservedObject var counter: Counter
    //省略

(新)iOS 17からのObservationを使った書き方

struct CounterView: View {
    // @ObservedObjectをつけない
    var counter: Counter

(旧)iOS 16までの@StateObject

struct CounterView2: View {
    @StateObject private var counter = Counter()
    //省略

iOS 17からのObservationを使った書き方

struct CounterView2: View {
    // @StateObjectじゃない
    @State private var counter = Counter()

違いはApple公式がすごくわかりやすいです。

Migrating from the Observable Object protocol to the Observable macro

あと、ハマリポイントとしては、CounterViewでStepperやToggleにプロパティを渡したいので以下のように@Bindableで宣言しなおすところでした。

struct CounterView: View {
    var counter: Counter

    var body: some View {
        // $counterを使いたいので@Bindableで宣言
        // そうでなければこの記述は不要
        @Bindable var counter = counter

        VStack(alignment: .leading, spacing: 10) {
            Stepper("count \(counter.count)", value: $counter.count)

という感じです。ではでは。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.