-
Notifications
You must be signed in to change notification settings - Fork 147
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:
|
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.