-
Notifications
You must be signed in to change notification settings - Fork 176
Expose observe overloads for separate tracking and application of changes
#286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
a8f3be2
5df4eb9
c058a10
a1573d6
ef2926b
684393d
c37705e
189e51a
dd42f44
03a0238
cfd5b88
994a415
5937275
f105a85
52c254a
07b5cdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -110,6 +110,52 @@ | |||||
| observe { _ in apply() } | ||||||
| } | ||||||
|
|
||||||
| /// Observe access to properties of an observable (or perceptible) object. | ||||||
| /// | ||||||
| /// 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 | ||||||
| /// - Parameter onChange: Invoked when the value of a property changes | ||||||
| /// - Returns: A cancellation token. | ||||||
| @discardableResult | ||||||
| public func observe( | ||||||
| _ tracking: @escaping @MainActor @Sendable () -> Void, | ||||||
| onChange apply: @escaping @MainActor @Sendable () -> Void | ||||||
| ) -> ObserveToken { | ||||||
| observe { _ in apply() } | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why doesn't this function body ignore the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed it has to be used and I fixed it in my local version a while ago, but didn't push it, I'll push an update tomorrow |
||||||
| } | ||||||
|
|
||||||
| /// Observe access to properties of an observable (or perceptible) object. | ||||||
| /// | ||||||
| /// A version of ``observe(_:)`` 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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. though, in general, I would promote the label onChange to be the parameter name and get rid of |
||||||
| /// - Returns: A cancellation token. | ||||||
| @discardableResult | ||||||
| public func observe( | ||||||
| _ tracking: @escaping @MainActor @Sendable (_ transaction: UITransaction) -> Void, | ||||||
| onChange apply: @escaping @MainActor @Sendable (_ transaction: UITransaction) -> Void | ||||||
| ) -> ObserveToken { | ||||||
| let token = SwiftNavigation.observe { transaction in | ||||||
| MainActor._assumeIsolated { | ||||||
| tracking(transaction) | ||||||
| } | ||||||
| } onChange: { transaction in | ||||||
| MainActor._assumeIsolated { | ||||||
| apply(transaction) | ||||||
| } | ||||||
| } task: { transaction, work in | ||||||
| DispatchQueue.main.async { | ||||||
| withUITransaction(transaction, work) | ||||||
| } | ||||||
| } | ||||||
| tokens.append(token) | ||||||
| return token | ||||||
| } | ||||||
|
|
||||||
| /// Observe access to properties of an observable (or perceptible) object. | ||||||
| /// | ||||||
| /// A version of ``observe(_:)`` that is passed the current transaction. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import SwiftNavigation | ||
| import Perception | ||
| import XCTest | ||
|
|
||
| class NestingObserveTests: XCTestCase { | ||
| #if swift(>=6) | ||
| func testIsolation() async { | ||
| await MainActor.run { | ||
| var count = 0 | ||
| let token = SwiftNavigation.observe { | ||
| count = 1 | ||
| } | ||
| XCTAssertEqual(count, 1) | ||
| _ = token | ||
| } | ||
| } | ||
| #endif | ||
|
|
||
| #if !os(WASI) | ||
| @MainActor | ||
| func testNestedObservation() async { | ||
| let object = ParentObject() | ||
| let model = ParentObject.Model() | ||
|
|
||
| MockTracker.shared.entries.removeAll() | ||
| object.bind(model) | ||
|
|
||
| XCTAssertEqual( | ||
| MockTracker.shared.entries.map(\.label), | ||
| [ | ||
| "ParentObject.bind", | ||
| "ParentObject.value.didSet", | ||
| "ChildObject.bind", | ||
| "ChildObject.value.didSet", | ||
| ] | ||
| ) | ||
|
|
||
| MockTracker.shared.entries.removeAll() | ||
| model.child.value = 1 | ||
|
|
||
| await Task.yield() | ||
|
|
||
| XCTAssertEqual( | ||
| MockTracker.shared.entries.map(\.label), | ||
| [ | ||
| "ChildObject.Model.value.didSet", | ||
| "ChildObject.value.didSet", | ||
| ] | ||
| ) | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| #if !os(WASI) | ||
| fileprivate class ParentObject: @unchecked Sendable { | ||
| var tokens: Set<ObserveToken> = [] | ||
| let child: ChildObject = .init() | ||
|
|
||
| var value: Int = 0 { | ||
| didSet { MockTracker.shared.track(value, with: "ParentObject.value.didSet") } | ||
| } | ||
|
|
||
| func bind(_ model: Model) { | ||
| MockTracker.shared.track((), with: "ParentObject.bind") | ||
|
|
||
| tokens = [ | ||
| observe { _ = model.value } onChange: { [weak self] in | ||
| self?.value = model.value | ||
| }, | ||
| observe { _ = model.child } onChange: { [weak self] in | ||
| self?.child.bind(model.child) | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| @Perceptible | ||
| class Model: @unchecked Sendable { | ||
| var value: Int = 0 { | ||
| didSet { MockTracker.shared.track(value, with: "ParentObject.Model.value.didSet") } | ||
| } | ||
|
|
||
| var child: ChildObject.Model = .init() { | ||
| didSet { MockTracker.shared.track(value, with: "ParentObject.Model.value.didSet") } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fileprivate class ChildObject: @unchecked Sendable { | ||
| var tokens: Set<ObserveToken> = [] | ||
|
|
||
| var value: Int = 0 { | ||
| didSet { MockTracker.shared.track(value, with: "ChildObject.value.didSet") } | ||
| } | ||
|
|
||
| func bind(_ model: Model) { | ||
| MockTracker.shared.track((), with: "ChildObject.bind") | ||
|
|
||
| tokens = [ | ||
| observe { _ = model.value } onChange: { [weak self] in | ||
| self?.value = model.value | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| @Perceptible | ||
| class Model: @unchecked Sendable { | ||
| var value: Int = 0 { | ||
| didSet { MockTracker.shared.track(value, with: "ChildObject.Model.value.didSet") } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fileprivate final class MockTracker: @unchecked Sendable { | ||
| static let shared = MockTracker() | ||
|
|
||
| struct Entry { | ||
| var label: String | ||
| var value: Any | ||
| } | ||
|
|
||
| var entries: [Entry] = [] | ||
|
|
||
| init() {} | ||
|
|
||
| func track( | ||
| _ value: Any, | ||
| with label: String | ||
| ) { | ||
| entries.append(.init(label: label, value: value)) | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.