Swiftの@State、@Published、@ObservedObject、@StateObjectとは

@Stateは値が変わるとViewを更新する仕組みのことです

以下のようにcount変数に値を入れると自動的にViewが更新されます。

import SwiftUI

struct CounterView3: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

@Stateはプリミティブ型だけではなくObject型でもOKです。

ちなみに@StateがSingletonクラスの値を参照した場合は以下のようになります。

class Singleton {
    static let shared = Singleton()
    var value: String = "Default Value"
}

struct ContentView3: View {
    @State private var singletonValue = Singleton.shared.value

    var body: some View {
        VStack {
            Text("Singleton Value: \(singletonValue)")
            Button("Update Singleton Value") {
                // Singletonクラスの更新だけではViewは反映されない
                Singleton.shared.value = "New Value"
                // これも必要
                singletonValue = Singleton.shared.value
            }
        }
    }
}

Singletonクラスの値を更新しただけではViewは更新されず、@Stateで定義した変数を更新する必要があります。これは2度手間でSingletonクラスを更新したらViewも更新されて欲しいですよね?

その場合は@Publishedの方を使います。

@Publishedとはアプリの状態に従ってViewを更新する仕組みのことです

以下のサンプルコードを見てください。ボタンをタップするとViewが更新されて数字が増えていきます。

class MyViewModel: ObservableObject {
    @Published var count: Int = 0
    
    func increment() {
        count += 1
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = MyViewModel()
    
    var body: some View {
        VStack {
            Text("count: \(viewModel.count)")
            
            Button(action: {
                viewModel.increment()
            }) {
                Text("ボタン")
            }
        }
    }
}

この

@Published var count: Int = 0

に値を入れるとViewが更新されるというわけです。@PublishはObservableObjectプロトコルに準拠したクラスのフィールドに付与できます。structには付与できません。

@Publishedは値の監視として使うこともできます

sinkを使えばcountの値を監視することができます。以下のサンプルコードではCounterをシングルトンクラスにして、CounterObserverがcountの値が変わる度に通知を受け取ることができます。

class Counter: ObservableObject {
    public static let shared = Counter()

    @Published var count: Int = 0
    
    private init() {}
    
    func increment() {
        count += 1
    }
}

class CounterObserver {
    private var cancellable: AnyCancellable?
    
    init(){
       cancellable = Counter.shared.$count.sink { count in
           // countの値が変わる度に通知が来てログ出力される.
           print ("\(count)")
        }
    }
    
    func cancel(){
        // 監視をキャンセルする.
        cancellable?.cancel()
    }
}

監視をやめたい時はsinkの戻り値であるAnyCancellableをcancelすると監視を終了できます。

@ObservedObjectと@StateObjectの違い

以下のようなContentViewが親ViewでStateObjectCounterとObservedObjectCounterが子Viewの関係性であった場合

import SwiftUI

struct ContentView: View {
    @State var counter = 0
    var body: some View {
        VStack(alignment: .leading, spacing: 50) {
            HStack {
                Text("counter: \(counter)")
                Button("+") {
                    counter += 1
                }
            }
            StateObjectCounter()
            ObservedObjectCounter()
        }
    }
}

final class Counter: ObservableObject {
    @Published var number = 0
}

struct StateObjectCounter: View {
    @StateObject private var counter = Counter()
    var body: some View {
        HStack {
            Text("StateObjectCounter: \(counter.number)")
            Button("+") {
                counter.number += 1
            }
        }
    }
}

struct ObservedObjectCounter: View {
    @ObservedObject private var counter = Counter2()
    
    var body: some View {
        HStack {
            Text("OvservedObjectCounter: \(counter.number)")
            Button("+") {
                counter.number += 1
            }
        }
    }
}

親Viewのincrementを行って再描画をするとObservedObjectCounterのカウンターは0になりStateObjectCounterのカウンターは何も変わりません。つまり親Viewの再描画をした時にアノテーションで指定したクラスのインスタンスが再生成されるかどうか、それが違いです。