😀

@Observableで監視した値をView以外で受け取る方法

2024/08/22に公開

はじめに

@Observableが便利。
と言いつつ、SwiftUIをVisionOS開発から触っているので、元々のCombineを利用したObservableObjectプロトコルで値を監視する方法をほぼ知らないまま@Observable(Observation)を利用していますが、通常の利用方法ならばとても便利だなーと思っています。公式の動画を見てみてもコードがスッキリしています。
ただ、SwiftUIの文脈以外、つまりView以外で値を受け取る方法は、公式動画では紹介されておらず、調べてみるとCombineよりも扱いに工夫が必要でした。

Viewで更新を受け取る場合

@ObservableをView以外で利用したいと思うタイミングは、View用に@Observableで監視している値をView以外でも利用したい場合だと思います。下のようなTimerでnumberが更新されるNumberProviderをViewで利用しており、さらに他の場所で利用する場合を考えます。

import Observation

@Observable
class NumberProvider {
    public var number: Int = 0

    init(){
        startIncrementing()
    }
    func startIncrementing() {
        Task {
            while true {
                try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒スリープ
                number += 1
                print("intProperty: \(number)")
            }
        }
    }
}
import SwiftUI
import RealityKit
import RealityKitContent

struct NumCountView: View {

    let numProvider = NumberProvider()

    var body: some View {
        VStack {
            Model3D(named: "Scene", bundle: realityKitContentBundle)
                .padding(.bottom, 50)

            Text("count\(numProvider.number)")
        }
        .padding()
        .onAppear(){
        }
    }
}

View以外で受け取る場合

Combineで同じように値を共有する場合は、例えばsinkを利用して値の更新を見ることができるかと思います。Observationでこれに当たるものは、withObservationTracking(_:onChange:)になります。下のコードでは、ObserveNumクラスがNumberProviderのnumberの値の更新を見ています。

import SwiftUI
import Observation

@Observable
class ObserveNum {
    var numberProvider = NumberProvider()

    init() {
        tracking()
    }

    private func tracking() {
        withObservationTracking {
            // このクロージャーの中に存在する値を監視対象とする。
            print("value\(numberProvider.number)")
        } onChange: {
            // ここに変更時の実行内容を書く
            print("onChange")
            // 永続させるために、再度withObservationTrackingを登録する。
            Task { @MainActor [weak self] in
                guard let self else { return }
                tracking()
            }
        }
    }
}

withObservationTrackingの中身

observeNumクラスでは、trackingメソッドでwithObservationTrackingを実行しています。withObservationTrackingの最初のクロージャーでは、監視対象を決定します。監視対象の値を明示的に設定する必要はなく、クロージャーの中利用してさえいれば勝手に監視対象になってくれます。今回の場合はnumberProvider.numberをprint内で利用していることで監視対象となります。この監視対象の値が変更されるとonChangeの中の一度だけ実行されます。一度だけ実行されることがポイントで、永続的に監視することはできません。
Task内でtrackingを再帰的に呼んでいるのは永続的に監視するために、再度withObservationTrackingを実行し値を監視するためです。これによって、onChange実行時に再度numberProvider.numberの監視を始めることで擬似的に永続的に監視できるようにしています。

なんかめんどくさくない?これが本当に正式な方法なの?

Combineにはsinkというとても便利なものがあるのだから、Observationにもいい感じのものがないのか!と思った方いると思いますが。残念ながら、今はまだないようです。(フォーラムでも同じような質問がありましたが、I've seen that doesn't smell rightという表現でなんか正しくない気がする。。みたいなことを話していました)

今後に期待ですね。

おわりに

いかがだったでしょうか、Combineの方がまだコードが綺麗になる場面もあるのかもしれませんが、@Observableをつけるだけで、値を勝手に監視してくれるのは開発体験としてとても良いもので、できればこれを使っていけたらいいなと思っています。

VisioOSの開発者は、サンプルが基本@Observableを利用しているのでこれに向き合う必要はあるだろうなと思います。また、2024年8/22日現在ではChatGPTだと@Observableの情報はインプットされていないようでふさわしい内容が返ってこないので、先人の記事や公式ドキュメントにとても助けられました。いつも先人の皆様、ありがとうございます!この記事も誰かの役に立ったら幸いです。

参考になった記事

https://qiita.com/usamik26/items/6544879e3aa8343b709f

https://developer.apple.com/videos/play/wwdc2023/10149/

https://developer.apple.com/forums/thread/746466?answerId=779766022#779766022

Discussion