Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions Sources/SwiftNavigation/NSObject+Observe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/// any accessed fields so that the view is always up-to-date.
///
/// It is most useful when dealing with non-SwiftUI views, such as UIKit views and controller.
/// You can invoke the ``observe(_:)`` method a single time in the `viewDidLoad` and update all
/// You can invoke the ``observe(_:)-(()->Void)`` method a single time in the `viewDidLoad` and update all
/// the view elements:
///
/// ```swift
Expand All @@ -37,7 +37,7 @@
/// ever mutated, this trailing closure will be called again, allowing us to update the view
/// again.
///
/// Generally speaking you can usually have a single ``observe(_:)`` in the entry point of your
/// Generally speaking you can usually have a single ``observe(_:)-(()->Void)`` in the entry point of your
/// view, such as `viewDidLoad` for `UIViewController`. This works even if you have many UI
/// components to update:
///
Expand All @@ -64,7 +64,7 @@
/// a label or the `isHidden` of a button.
///
/// However, if there is heavy work you need to perform when state changes, then it is best to
/// put that in its own ``observe(_:)``. For example, if you needed to reload a table view or
/// put that in its own ``observe(_:)-(()->Void)``. For example, if you needed to reload a table view or
/// collection view when a collection changes:
///
/// ```swift
Expand Down Expand Up @@ -106,13 +106,36 @@
/// of a property changes.
/// - Returns: A cancellation token.
@discardableResult
public func observe(_ apply: @escaping @MainActor @Sendable () -> Void) -> ObserveToken {
public func observe(
_ apply: @escaping @MainActor @Sendable () -> Void
) -> ObserveToken {
observe { _ in apply() }
}

/// Observe access to properties of an observable (or perceptible) object.
///
/// A version of ``observe(_:)`` that is passed the current transaction.
/// This tool allows you to set up an observation loop so that you can access fields from an
/// observable model in order to populate your view, and also automatically track changes to
/// any fields accessed in the tracking parameter so that the view is always up-to-date.
///
/// - Parameter tracking: A closure that contains properties to track

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - Parameter tracking: A closure that contains properties to track
/// - Parameter context: A closure that contains properties to track

/// - Parameter onChange: Invoked when the value of a property changes
/// - Returns: A cancellation token.
@discardableResult
public func observe(
_ context: @escaping @MainActor @Sendable () -> Void,
onChange apply: @escaping @MainActor @Sendable () -> Void
) -> ObserveToken {
observe { _ in
context()
} onChange: { _ in
apply()
}
}

/// Observe access to properties of an observable (or perceptible) object.
///
/// A version of ``observe(_:)-(()->Void)`` that is passed the current transaction.
///
/// - Parameter apply: A closure that contains properties to track and is invoked when the value
/// of a property changes.
Expand All @@ -134,6 +157,35 @@
return token
}

/// Observe access to properties of an observable (or perceptible) object.
///
/// A version of ``observe(_:onChange:)-(()->Void,_)`` that is passed the current transaction.
///
/// - Parameter tracking: A closure that contains properties to track
/// - Parameter onChange: Invoked when the value of a property changes
/// - Returns: A cancellation token.
@discardableResult
public func observe(
_ context: @escaping @MainActor @Sendable (_ transaction: UITransaction) -> Void,
onChange apply: @escaping @MainActor @Sendable (_ transaction: UITransaction) -> Void
) -> ObserveToken {
let token = SwiftNavigation._observe { transaction in
MainActor._assumeIsolated {
context(transaction)
}
} onChange: { transaction in
MainActor._assumeIsolated {
apply(transaction)
}
} task: { transaction, work in
DispatchQueue.main.async {
withUITransaction(transaction, work)
}
}
tokens.append(token)
return token
}

fileprivate var tokens: [Any] {
get {
objc_getAssociatedObject(self, Self.tokensKey) as? [Any] ?? []
Expand Down
201 changes: 182 additions & 19 deletions Sources/SwiftNavigation/Observe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import ConcurrencyExtras
#if swift(>=6)
/// Tracks access to properties of an observable model.
///
/// This function allows one to minimally observe changes in a model in order to
/// This function is a convenient variant of ``observe(_:onChange:)-(()->Void,_)`` that
/// combines tracking context and onChange handler in one `apply` argument
/// and allows one to minimally observe changes in a model in order to
/// react to those changes. For example, if you had an observable model like so:
///
/// ```swift
Expand Down Expand Up @@ -50,24 +52,87 @@ import ConcurrencyExtras
///
/// And you can also build your own tools on top of `observe`.
///
/// - Parameters:
/// - isolation: The isolation of the observation.
/// - apply: A closure that contains properties to track.
/// - Parameter apply: A closure that contains properties to track.
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
/// is deallocated.
@inlinable
public func observe(
@_inheritActorContext _ apply: @escaping @isolated(any) @Sendable () -> Void
@_inheritActorContext
_ apply: @escaping @isolated(any) @Sendable () -> Void
) -> ObserveToken {
observe { _ in Result(catching: apply).get() }
}

/// Tracks access to properties of an observable model.
///
/// A version of ``observe(isolation:_:)`` that is handed the current ``UITransaction``.
/// This function allows one to minimally observe changes in a model in order to
/// react to those changes. For example, if you had an observable model like so:
///
/// ```swift
/// @Observable
/// class FeatureModel {
/// var count = 0
/// }
/// ```
///
/// Then you can use `observe` to observe changes in the model. For example, in UIKit you can
/// update a `UILabel`:
///
/// ```swift
/// observe { [model] in model.count } onChange: { [countLabel, model] in
/// countLabel.text = "Count: \(model.count)"
/// }
/// ```
///
/// - Parameters:
/// - isolation: The isolation of the observation.
/// - apply: A closure that contains properties to track.
/// Anytime the `count` property of the model changes the trailing closure will be invoked again,
/// allowing you to update the view. Further, only changes to properties accessed in the trailing
/// closure will be observed.
///
/// > Note: If you are targeting Apple's older platforms (anything before iOS 17, macOS 14,
/// > tvOS 17, watchOS 10), then you can use our
/// > [Perception](http://github.com/pointfreeco/swift-perception) library to replace Swift's
/// > Observation framework.
///
/// This function also works on non-Apple platforms, such as Windows, Linux, Wasm, and more. For
/// example, in a Wasm app you could observe changes to the `count` property to update the inner
/// HTML of a tag:
///
/// ```swift
/// import JavaScriptKit
///
/// var countLabel = document.createElement("span")
/// _ = document.body.appendChild(countLabel)
///
/// let token = observe { model.count } onChange: {
/// countLabel.innerText = .string("Count: \(model.count)")
/// }
/// ```
///
/// And you can also build your own tools on top of `observe`.
///
/// - Parameter context: A closure that contains properties to track.
/// - Parameter apply: Invoked when the value of a property changes
/// > `onChange` is also invoked on initial call
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
@inlinable
public func observe(
@_inheritActorContext
_ context: @escaping @isolated(any) @Sendable () -> Void,
@_inheritActorContext
onChange apply: @escaping @isolated(any) @Sendable () -> Void
) -> ObserveToken {
observe(
{ _ in Result(catching: context).get() },
onChange: { _ in Result(catching: apply).get() }
)
}

/// Tracks access to properties of an observable model.
///
/// A version of ``observe(_:)-(()->Void)`` that is handed the current ``UITransaction``.
///
/// - Parameter apply: A closure that contains properties to track.
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
public func observe(
Expand All @@ -83,25 +148,115 @@ import ConcurrencyExtras
}
)
}

/// Tracks access to properties of an observable model.
///
/// A version of ``observe(_:onChange:)-(()->Void,_)`` that is handed the current ``UITransaction``.
///
/// - Parameter context: A closure that contains properties to track.
/// - Parameter apply: Invoked when the value of a property changes
/// > `onChange` is also invoked on initial call
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
public func observe(
@_inheritActorContext
_ context: @escaping @isolated(any) @Sendable (_ transaction: UITransaction) -> Void,
onChange apply: @escaping @isolated(any) @Sendable (_ transaction: UITransaction) -> Void
) -> ObserveToken {
_observe(
context,
onChange: apply,
task: { transaction, operation in
Task {
await operation()
}
}
)
}
#endif

/// Observes changes in given context
///
/// - Parameter apply: Invoked when a change occurs in observed context
/// > `apply` is also invoked on initial call
/// - Parameter task: The task that wraps recursive observation calls
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
func _observe(
_ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
task: @escaping @Sendable (
_ transaction: UITransaction, _ operation: @escaping @Sendable () -> Void
_ transaction: UITransaction,
_ operation: @escaping @Sendable () -> Void
) -> Void = {
Task(operation: $1)
}
) -> ObserveToken {
return SwiftNavigation.onChange(
of: apply,
perform: apply,
task: task
)
}

/// Observes changes in given context
///
/// - Parameter context: Observed context
/// - Parameter apply: Invoked when a change occurs in observed context
/// > `onChange` is also invoked on initial call
/// - Parameter task: The task that wraps recursive observation calls
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
func _observe(
_ context: @escaping @Sendable (_ transaction: UITransaction) -> Void,
onChange apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
task: @escaping @Sendable (
_ transaction: UITransaction,
_ operation: @escaping @Sendable () -> Void
) -> Void = {
Task(operation: $1)
}
) -> ObserveToken {
let token = SwiftNavigation.onChange(
of: context,
perform: apply,
task: task
)

apply(.current)
return token
}

/// Observes changes in given context
///
/// - Parameter context: Observed context
/// - Parameter operation: Invoked when a change occurs in observed context
/// > `operation` is not invoked on initial call
/// - Parameter task: The task that wraps recursive observation calls
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
func onChange(
of context: @escaping @Sendable (_ transaction: UITransaction) -> Void,
perform operation: @escaping @Sendable (_ transaction: UITransaction) -> Void,
task: @escaping @Sendable (
_ transaction: UITransaction,
_ operation: @escaping @Sendable () -> Void
) -> Void = {
Task(operation: $1)
}
) -> ObserveToken {
let token = ObserveToken()
onChange(
{ [weak token] transaction in
SwiftNavigation.withRecursivePerceptionTracking(
of: { [weak token] transaction in
guard let token, !token.isCancelled else { return }
context(transaction)
},
perform: { [weak token] transaction in
guard
let token,
!token.isCancelled
else { return }

var perform: @Sendable () -> Void = { apply(transaction) }
var perform: @Sendable () -> Void = { operation(transaction) }
for key in transaction.storage.keys {
guard let keyType = key.keyType as? any _UICustomTransactionKey.Type
else { continue }
Expand All @@ -121,17 +276,25 @@ func _observe(
return token
}

private func onChange(
_ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
private func withRecursivePerceptionTracking(
of context: @escaping @Sendable (_ transaction: UITransaction) -> Void,
perform operation: @escaping @Sendable (_ transaction: UITransaction) -> Void,
task: @escaping @Sendable (
_ transaction: UITransaction, _ operation: @escaping @isolated(any) @Sendable () -> Void
_ transaction: UITransaction,
_ operation: @escaping @Sendable () -> Void
) -> Void
) {
withPerceptionTracking {
apply(.current)
context(.current)
} onChange: {
task(.current) {
onChange(apply, task: task)
operation(.current)

withRecursivePerceptionTracking(
of: context,
perform: operation,
task: task
)
}
}
}
Expand Down
34 changes: 0 additions & 34 deletions Tests/SwiftNavigationTests/ObserveTests.swift

This file was deleted.

Loading
Loading