[Tip] - An utility view to improve the performance of ForEachStore
in some situations.
#1595
tgrapperon
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Hello!
When using a top-level
ViewStore
as an@ObservedObject
,@StateObject
or even when using aWithViewStore
, the invalidation scope of this object interfere with more complexView
whose dependencies are out of theViewStore
scope. This can happen for example when using aForEachStore
, as the content is usually a standalone view that only depends onEachStore
. If we have for example:and then in the
View
:Using the slider invalidates the whole
WithViewStore
content. This in turn causes the invalidation of the wholeScrollView
itself, as all these views are using closures to define their content. Ultimately, the escaping closure ofForEachStore
changes its reference, dirtying all its closure-based containers (that isLazyHStack
andScrollView
). We would have the same issue with vanilla SwiftUI, asForEach
uses itself an escaping closure. Please also note that the problem is not univocally created by escaping closures, as it can happen relatively easily as soon as reference types are involved, explicitly or internally.The first workaround is to narrow
WithViewStore
's reach:But you can remark that we had to refactor our view hierarchy to put the
VStack
out ofWithViewStore
. This kind of modification is not always convenient to perform, especially if the "heavy" view is deep into the belly ofWithViewStore
's hierarchy. And when using@ObservedObject
or@StateObject
instead, it is impossible.The "clean" workaround is to extract the
ScrollView
and its content in a standalone view:and use
ChildrenView(store: store)
in place of theScrollView
inWithViewStore
. But please note that this view is not expecting aStore<IdentifiedArrayOf<Child.State,…
, and it performs the scope internally. This is because scoping is currently not reusingStore
instances, so if we scope at theWithViewStore
level, we will vend a newStore
instance. This will invalidateChildrenView
dependencies, and runs itsbody
, bringing us back to where we started.So there is a fair amount of finesse required to work around this issue. One solution is to cook up a wrapper view that depends only on the store's instance:
I can discuss the implementation in the comments if you want, but in few words, it captures the store's instance and its related content. If and only if the store instance changes, the content is invalidated. You simply wrap the view you want to isolate:
This view will not be invalidated when
ViewStore
emits itsobjectWillChange
. We can make it even more resilient by removing the.id(ObjectIdentifier(store))
step. In this case, it won't be invalidated even when the store instance changes (which should probably be OK).Please note that this wrapper isolates its content from the parent view:
@Environment
values orviewStore
can be captured, but they won't be observed. If you "StoreIsolate
" some view, it is really isolated, and its content will only reflect itsstore
's changes.There are other approaches to this problem. For example, one can define the
Isolated
ViewModifier
:You activate it simply by tacking
.isolated()
after the view you want to isolate. The problem with this approach is that we snapshot blindly the content view only once, so it is equivalent to removing the.id(ObjectIdentifier(store))
inStoreIsolated
and it won't followStore
instances changes. As I don't know if this is really a problem yet, I prefer to keep a store-instance-dependent approach. But it is clear that.isolated()
makes more sense thatStoreIsolated
if the.id
part is ultimately dropped.It is also possible that using
@StateObject
allows further improvements in this area, as we can use it to construct a genericwhere the
Store<ScopedState, ScopedAction>
argument would be a stable instance. Because we won't have to follow them anymore (if it's even required), this would make.isolated()
the best solution to the isolation problem.I'm not proposing any PR yet, but this discussion may help give a little visibility to this potentially subtle problem.
Beta Was this translation helpful? Give feedback.
All reactions