-
Notifications
You must be signed in to change notification settings - Fork 159
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?
Conversation
@stephencelis @mbrandonw Looking forward for your review 🫠 |
Hi @maximkrouk, I'm sorry but I still don't really understand what you're are trying to achieve here, and why it is any better than just using Also, have you tried implementing these tools outside of the library? If not, can you try? And if you run into problems can you let us know what those are? |
Here is a simplified example in an executable swift package maximkrouk/swift-navigation-test-repo:simplified The issue is that nested observe applictions are triggering parent ones. This behavior is expected because the Redundant updates are present for any amount of nested changes so it'll be a pretty bit problem in a larger application structure like this (pseudocode) If we set appView.setModel(appModel) where func setModel(_ model:) {
observe {
childView.setModel(model.child) // will call observe for child props and probably `setModel` for child.child etc.
}
} then AppView { // ← this
MainView { // ← this
Header { // ← this
Labels { // ← and this update will be triggered
UsernameLabel() // ← for a simple text change here
}
}
}
} Implementing smth similar outside the lib requires jumping through hoops, I did smth similar utilizing And last but not least I genuinely believe that adding Some examples of what people might do for convenience // autoclosure-based observation
func observe<Value>(
_ value: @Sendable @escaping @autoclosure () -> Value,
onChange: @Sendable @escaping (Value) -> Void
) -> ObserveToken {
SwiftNavigation.observe { _ = value() } onChange: {
onChange(value())
}
}
observe(myModel.text) { label.text = $0 } // KeyPath-based observation with weak capture
func observeWeak<Object: AnyObject & Perceptible & Sendable, Value>(
_ object: Object,
_ keyPath: KeyPath<Object, Value> & Sendable,
onChange: @Sendable @escaping (Value) -> Void
) -> ObserveToken {
SwiftNavigation.observe { [weak object] in
_ = object?[keyPath: keyPath]
} onChange: { [weak object] in
object.map { onChange($0[keyPath: keyPath]) }
}
}
observeWeak(myModel, \.text) { label.text = $0 } Oh and
it's not just better, it allows to correctly process nested observables (this is possible with pure |
I was trying to cover the issue extensively, but tldr is:
|
@maximkrouk Revisiting this I think we're down to merge this! I merged |
_ tracking: @escaping @MainActor @Sendable () -> Void, | ||
onChange apply: @escaping @MainActor @Sendable () -> Void | ||
) -> ObserveToken { | ||
observe { _ in apply() } |
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.
Why doesn't this function body ignore the tracking
parameter? Could we have a unit test to check that this actually works?
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.
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
Wow, it's been a while, but I'm glad anyway 😅 I'll take a look and push an update tomorrow |
- Fix Xcode26 warnings related to redundant use of @_inheritActorContext - Fix NSObject.observe
I left a comment related to this branch here #306 (comment) |
@maximkrouk Replied, but the short of it is you can ignore those warnings! |
I fixed comments and updated tests but now there are 2 other issues:
I ran out of time today, so I'll take a look on it tomorrow, but from high level overview of my changes it doesn't look like it should break anything (especially for the old signature with one closure) 🤔 |
…pose-observe-function # Conflicts: # Sources/SwiftNavigation/Observe.swift
2886926
to
684393d
Compare
XCTAssertEqual( | ||
MockTracker.shared.entries.withValue(\.count), | ||
13 | ||
) |
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.
The order of logs is not consistent here, so I decided to assert by the count which seems stable across runs
UPD:
|
Task { | ||
await operation() | ||
} | ||
call(operation) |
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.
The task here was breaking the isolation, maybe marking everything as @_inheritActorContext
+ @isolated(any)
wasn't required, I'll check if it works without it later 🫠
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.
Btw Task.yield()
is still not sufficient for awaiting for changes to be applied 😔
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.
Btw Task.yield()
is still not sufficient for awaiting for changes to be applied 😔
I also noticed that old observe(_:)
should use a separate version of onChange
/withRecursivePerceptionTracking
, current one is calling this shared apply
closure too often I'll investigate it a bit later 🫠
Task { | ||
await operation() | ||
} | ||
call(operation) |
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.
Btw Task.yield()
is still not sufficient for awaiting for changes to be applied 😔
Task { | ||
await operation() | ||
} | ||
call(operation) |
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.
Btw Task.yield()
is still not sufficient for awaiting for changes to be applied 😔
I also noticed that old observe(_:)
should use a separate version of onChange
/withRecursivePerceptionTracking
, current one is calling this shared apply
closure too often I'll investigate it a bit later 🫠
@Sendable | ||
private func call(_ f: @escaping @Sendable () -> Void) { | ||
f() | ||
} |
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.
Workarounds to make @escaping @isolated(any) @Sendable () -> Void
functions callable from sync contexts
TODO: remove
@escaping
It does look like we need to bring it back (#316) but we still want |
Follow-up PR for #281
The issue:
In code like this:
where
call stack will look kinda like this
And since child props access is nested in
observe { // #1
, parent will applyself.childComponent.bind(model.child)
on any child props change even tho only child should be updated in that case viaobserve { // #2
Proposed solution
Provide an
observe
overload that will track and apply changes separately so the pseudocode from above will look like this:call stack will look kinda like this
Final note
Basically the library adds a cool handling of UITransactions and resubscription to
withPerceptionTracking
but cuts down the ability to separate tracking and updates which is present inwithPerceptionTracking
. The solution cannot be replaced with a simple use ofwithPerceptionTracking
since UITransaction-related stuff is library implementation detail, this PR keeps existing functionality, but brings back a lower-levelwithPerceptionTracking
-like API keeping the cool stuff related toUITransaction
. The API is not as ergonomic as a basicobserve
but it handles an important edgecase and it's sufficient for users of the library to build their own APIs based on the new method.