cockscomblog?

cockscomb on hatena blog

SwiftUIのDynamicPropertyを試す

SwiftUIにはDynamicPropertyというprotocolがある。

これを使ってみようという趣旨の記事を見かけた。

ので、私も試してみました。

@Now

import Combine
import SwiftUI

class Clock: ObservableObject {
    @Published private(set) var date: Date = Date()

    init() {
        Timer.publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .assign(to: &$date)
    }
}

@propertyWrapper
struct Now: DynamicProperty {
    @StateObject private var clock = Clock()

    var wrappedValue: Date {
        get {
            clock.date
        }
    }
}

こういうのを作っておいて

import SwiftUI

struct ContentView: View {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .none
        formatter.timeStyle = .long
        return formatter
    }()

    @Now private var date: Date

    var body: some View {
        Text(date, formatter: Self.dateFormatter)
    }
}

こう使う。

これでContentViewは毎秒更新され、現在時刻を表示し続ける。

DynamicPropertyって何

ドキュメントには、Viewが再計算される前にDynamicPropertyupdateメソッドを呼んでくれるくらいの情報しかないが、実際にはもうちょっといろいろあるようだ。

上記の例でもわかるように、@ObservedObjectの更新によってViewが再描画される。

ここで急に、SwiftUIのCore Dataサポートのように、@FetchRequest的な形で何かできるんじゃないか、と気づくと思う。

実際にRealmにはそういう機能があった。

現実のユースケース

DynamicPropertyでは、StateなりStateObject(またはObservedObject)なり、あるいはEnvironment(やEnvironmentObject)を、Viewのpropertyで使えるようだ。

これは少しReact Hooksに似ている。ReactのuseStateとSwiftUIの@Stateの対称性のように、ReactのCustom Hookとちょっと似ている。

とはいえ、@Nowの例は、単にObservableObjectをそのまま使えばいいわけで、独自のDynamicPropertyを作ることが正当化されるような場面は稀かも。