@@ -62,6 +62,72 @@ import ConcurrencyExtras
6262 observe ( isolation: isolation) { _ in apply ( ) }
6363 }
6464
65+ /// Tracks access to properties of an observable model.
66+ ///
67+ /// This function allows one to minimally observe changes in a model in order to
68+ /// react to those changes. For example, if you had an observable model like so:
69+ ///
70+ /// ```swift
71+ /// @Observable
72+ /// class FeatureModel {
73+ /// var count = 0
74+ /// }
75+ /// ```
76+ ///
77+ /// Then you can use `observe` to observe changes in the model. For example, in UIKit you can
78+ /// update a `UILabel`:
79+ ///
80+ /// ```swift
81+ /// observe { _ = model.value } onChange: { [weak self] in
82+ /// guard let self else { return }
83+ /// countLabel.text = "Count: \(model.count)"
84+ /// }
85+ /// ```
86+ ///
87+ /// Anytime the `count` property of the model changes the trailing closure will be invoked again,
88+ /// allowing you to update the view. Further, only changes to properties accessed in the trailing
89+ /// closure will be observed.
90+ ///
91+ /// > Note: If you are targeting Apple's older platforms (anything before iOS 17, macOS 14,
92+ /// > tvOS 17, watchOS 10), then you can use our
93+ /// > [Perception](http://github.com/pointfreeco/swift-perception) library to replace Swift's
94+ /// > Observation framework.
95+ ///
96+ /// This function also works on non-Apple platforms, such as Windows, Linux, Wasm, and more. For
97+ /// example, in a Wasm app you could observe changes to the `count` property to update the inner
98+ /// HTML of a tag:
99+ ///
100+ /// ```swift
101+ /// import JavaScriptKit
102+ ///
103+ /// var countLabel = document.createElement("span")
104+ /// _ = document.body.appendChild(countLabel)
105+ ///
106+ /// let token = observe { _ = model.count } onChange: {
107+ /// countLabel.innerText = .string("Count: \(model.count)")
108+ /// }
109+ /// ```
110+ ///
111+ /// And you can also build your own tools on top of `observe`.
112+ ///
113+ /// - Parameters:
114+ /// - isolation: The isolation of the observation.
115+ /// - tracking: A closure that contains properties to track.
116+ /// - onChange: A closure that is triggered after some tracked property has changed
117+ /// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
118+ /// is deallocated.
119+ public func observe(
120+ isolation: ( any Actor ) ? = #isolation,
121+ @_inheritActorContext _ tracking: @escaping @Sendable ( ) -> Void ,
122+ @_inheritActorContext onChange apply: @escaping @Sendable ( ) -> Void
123+ ) -> ObserveToken {
124+ observe (
125+ isolation: isolation,
126+ { _ in tracking ( ) } ,
127+ onChange: { _ in apply ( ) }
128+ )
129+ }
130+
65131 /// Tracks access to properties of an observable model.
66132 ///
67133 /// A version of ``observe(isolation:_:)`` that is handed the current ``UITransaction``.
@@ -87,6 +153,36 @@ import ConcurrencyExtras
87153 }
88154 )
89155 }
156+
157+
158+ /// Tracks access to properties of an observable model.
159+ ///
160+ /// A version of ``observe(isolation:_:)`` that is handed the current ``UITransaction``.
161+ ///
162+ /// - Parameters:
163+ /// - isolation: The isolation of the observation.
164+ /// - tracking: A closure that contains properties to track.
165+ /// - onChange: A closure that is triggered after some tracked property has changed
166+ /// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
167+ /// is deallocated.
168+ public func observe(
169+ isolation: ( any Actor ) ? = #isolation,
170+ @_inheritActorContext _ tracking: @escaping @Sendable ( UITransaction ) -> Void ,
171+ @_inheritActorContext onChange apply: @escaping @Sendable ( _ transaction: UITransaction ) -> Void
172+ ) -> ObserveToken {
173+ let actor = ActorProxy ( base: isolation)
174+ return observe (
175+ tracking,
176+ onChange: apply,
177+ task: { transaction, operation in
178+ Task {
179+ await actor . perform {
180+ operation ( )
181+ }
182+ }
183+ }
184+ )
185+ }
90186#endif
91187
92188private actor ActorProxy {
@@ -105,7 +201,8 @@ private actor ActorProxy {
105201func observe(
106202 _ apply: @escaping @Sendable ( _ transaction: UITransaction ) -> Void ,
107203 task: @escaping @Sendable (
108- _ transaction: UITransaction , _ operation: @escaping @Sendable ( ) -> Void
204+ _ transaction: UITransaction ,
205+ _ operation: @escaping @Sendable ( ) -> Void
109206 ) -> Void = {
110207 Task ( operation: $1)
111208 }
@@ -138,6 +235,45 @@ func observe(
138235 return token
139236}
140237
238+ func observe(
239+ _ tracking: @escaping @Sendable ( _ transaction: UITransaction ) -> Void ,
240+ onChange apply: @escaping @Sendable ( _ transaction: UITransaction ) -> Void ,
241+ task: @escaping @Sendable (
242+ _ transaction: UITransaction ,
243+ _ operation: @escaping @Sendable ( ) -> Void
244+ ) -> Void = {
245+ Task ( operation: $1)
246+ }
247+ ) -> ObserveToken {
248+ let token = ObserveToken ( )
249+ SwiftNavigation . onChange (
250+ of: tracking,
251+ perform: { [ weak token] transaction in
252+ guard
253+ let token,
254+ !token. isCancelled
255+ else { return }
256+
257+ var perform : @Sendable ( ) -> Void = { apply ( transaction) }
258+ for key in transaction. storage. keys {
259+ guard let keyType = key. keyType as? any _UICustomTransactionKey . Type
260+ else { continue }
261+ func open< K: _UICustomTransactionKey > ( _: K . Type ) {
262+ perform = { [ perform] in
263+ K . perform ( value: transaction [ K . self] ) {
264+ perform ( )
265+ }
266+ }
267+ }
268+ open ( keyType)
269+ }
270+ perform ( )
271+ } ,
272+ task: task
273+ )
274+ return token
275+ }
276+
141277private func onChange(
142278 _ apply: @escaping @Sendable ( _ transaction: UITransaction ) -> Void ,
143279 task: @escaping @Sendable (
@@ -153,6 +289,31 @@ private func onChange(
153289 }
154290}
155291
292+ private func onChange(
293+ of tracking: @escaping @Sendable ( _ transaction: UITransaction ) -> Void ,
294+ perform action: @escaping @Sendable ( _ transaction: UITransaction ) -> Void ,
295+ apply: Bool = true ,
296+ task: @escaping @Sendable (
297+ _ transaction: UITransaction ,
298+ _ operation: @escaping @Sendable ( ) -> Void
299+ ) -> Void
300+ ) {
301+ if apply { action ( . current) }
302+
303+ withPerceptionTracking {
304+ tracking ( . current)
305+ } onChange: {
306+ task ( . current) {
307+ onChange (
308+ of: tracking,
309+ perform: action,
310+ apply: true ,
311+ task: task
312+ )
313+ }
314+ }
315+ }
316+
156317/// A token for cancelling observation.
157318///
158319/// When this token is deallocated it cancels the observation it was associated with. Store this
0 commit comments