📝

iOSのObservationについて

2025/03/13に公開

Observationは、iOS 17で導入された非同期フレームワークです。
少し触ってみました。

https://developer.apple.com/documentation/observation
https://developer.apple.com/documentation/SwiftUI/Managing-model-data-in-your-app
https://developer.apple.com/documentation/SwiftUI/Migrating-from-the-observable-object-protocol-to-the-observable-macro

ObservationはGeometryReaderなどで頻繁に再描画される際に、メモリを圧迫してしまうらしいです。
https://qiita.com/musicline_developer/items/522f2b3308c8eacc7e9f

SwiftUIで動かしてみる

Observationはデータ側のクラスに@Observableをつけるといい感じに監視できるようになるようです。
CombineはObservableObjectを実装してプロパティに@Publishedをつければ良かったんですよね。
比較用にstructも用意しました。

// Combine
final class CombineCounter: ObservableObject {
    @Published var count: Int = 0
}

// Observation
@Observable
final class ObservationCounter {
    var count: Int = 0
}

struct StructCounter {
    var count: Int = 0
}

class ClassCounter {
    var count: Int = 0
}

汚いコードですがとりあえず動かしてみます。
このような見た目のカウンターを作ります。


struct ContentView: View {
    @StateObject private var combineState = CombineCounter()
    @State var observationState = ObservationCounter()
    @State var structState = StructCounter()
    @State var classState = ClassCounter()
    
    var body: some View {
        let _ = print("parent")
        VStack(alignment: .leading) {
            ChildView(
                combineState: combineState,
                observationState: observationState,
                structState: structState,
                classState: classState
            )
            IncrementButton(text: "Combine"){ combineState.count += 1}
            IncrementButton(text: "Observation"){ observationState.count += 1}
            IncrementButton(text: "Struct"){ structState.count += 1}
            IncrementButton(text: "Class"){ classState.count += 1}
        }
        .padding()
    
    }
}

struct ChildView: View {
    @ObservedObject var combineState: CombineCounter
    let observationState: ObservationCounter
    let structState: StructCounter
    let classState: ClassCounter
    var body: some View {
        let _ = print("child")
        Text("Combine: \(combineState.count)")
        Text("Observation: \(observationState.count)")
        Text("Struct: \(structState.count)")
        Text("Class: \(classState.count)")
    }
}

struct IncrementButton: View {
    let text: String
    let action: () -> Void
    var body: some View {
        let _ = print("IncrementButton: \(text)")
        HStack {
            Text(text)
            Button(action: action) {
                Image(systemName: "plus.circle.fill")
                    .font(.title)
            }
        }
    }
}

ぱっと見のCombineとObservationの違いとしては
CombineはChildView側で@ObservedObjectをつけないとChildViewが更新されないということです。
これによってChildView側はstateがstructなのかObservedObjectを意識しないといけません。
Observationはこれを意識せずに利用できるため嬉しいですね。

さらにボタンを押した際のViewの更新に違いがありました。
Combineとstructはparentが更新されれました

parent
child
IncrementButton: Combine
IncrementButton: Observation
IncrementButton: Struct

Observationはchildのみが更新されました

child

Combineやstructはそのオブジェクト自体を監視しているため
parentに当たるViewが更新されてしまいますが、
Observationは特に気を使わなくても
プロパティーを監視するようなので再描画される範囲が少ないようです。
この辺りもObservationが強いように思います。

SwiftUI外で動かしてみる

CombineはSwiftUI外でも使いやすい印象でした。
ObservationもSwiftUI外で動かしてみます

// Combine
var cancellable = Set<AnyCancellable>()
let subject = CurrentValueSubject<Int, Never>(0)

subject
    .sink { value in
        print("Subject:", value)
    }.store(in: &cancellable)

subject.value += 1
subject.value += 1
subject.value += 1

// Observation
let counter = CounterClass()

withObservationTracking {
    print("Observation: \(counter.count)")
} onChange: {
    print("Observation onCange: \(counter.count)")
}

counter.count += 1
counter.count += 1
counter.count += 1

出力は以下のとおりでした。

Subject: 0
Subject: 1
Subject: 2
Subject: 3
Observation: 0
Observation onCange: 0

withObservationTrackingでは一回しか値の変更を監視してくれないようです。
なので再帰にすれば監視してくれます。

func recursionTracking<T>(
    _ apply: @escaping () -> T,
    onChange: @escaping (() -> Void)
) {
    _ = withObservationTracking(apply, onChange: {
        onChange()
        recursionTracking(apply, onChange: onChange)
    })
}

let recursionCounter = CounterClass()

recursionTracking {
    print("recursionTracking: \(recursionCounter.count)")
} onChange: {
    print("recursionTracking onCange: \(recursionCounter.count)")
}

recursionCounter.count += 1
recursionCounter.count += 1
recursionCounter.count += 1

出力は以下のとおりでした。
新しくなる前の値がprintされてるので
どうやらwillSetのタイミングで実行されるようです。
なので最新の値を取得したい場合は気を使わないといけないですね。
ObservationをSwiftUI外で使うのは少し難しいのかもしれません。

recursionTracking: 0
recursionTracking onCange: 0
recursionTracking: 0
recursionTracking onCange: 1
recursionTracking: 1
recursionTracking onCange: 2
recursionTracking: 2
株式会社ソニックムーブ

Discussion