iOSのObservationについて
Observationは、iOS 17で導入された非同期フレームワークです。
少し触ってみました。
ObservationはGeometryReaderなどで頻繁に再描画される際に、メモリを圧迫してしまうらしいです。
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