Block objectWillChange on @EnvironmentObject

Is there any way to conditionally block an @EnvironmentObject property from receiving its objectWillChange publisher? I'm making a custom @Store property wrapper for SwiftUI, which has an @EnvironmentObject property which subscribes to the central store, but only updates the view when some certain state has changed (specified by a keypath in the property wrapper's initialiser).

It seems like whenever an @EnvironmentObject property inside a property wrapper receives an objectWillChange, DynamicProperty does its magic and reloads the view. So, to stop DynamicProperty from reloading the view when some unused state changes (it'd be really nice to have some more information on how DynamicProperty works – I've looked through Tokamak's implementation, and am none the wiser :sweat_smile:), I need to be able to block the objectWillChange publisher from ever arriving to the @EnvironmentObject. Since we can implement any custom publisher for objectWillChange, I was thinking of having it carry some type metadata about what state has changed, and then each @Store property wrapper can independently compare that to the given keypath and decide whether the objectWillChange should pass or not.

Is there any way to conditionally block the objectWillChange from arriving to the @EnvironmentObject to achieve this?

You already answered your own question: Create your own objectWillChange publisher. It will only emit when you explicitly send it a signal. And the view will only reload when this publisher emits.

1 Like

Right, but the publisher emits whenever the whole app state changes. It's each @Store property wrapper which chooses independently whether they want to let the objectWillChange pass or not depending on the state they're "lensing". The @Store has no notion of the current app state, it only knows which state it wants to listen to – so it can't tell the objectWillChange when to publish. Additionally, the objectWillChange doesn't know which @Store wants its state update, only that it needs to publish the state update whenever the state changes.

So I feel like the solution is to intercept the objectWillChange publisher on the @EnvironmentObject side, and decide there whether is should pass or not. I may be missing something though.

Another possibility is to customise how DynamicProperty detects changes, so that it only detects the fact that the @EnvironmentObject received an objectWillChange when I want it to – is that possible?

At this point I have absolutely no idea what you're talking about. Please see if you can write some example code.

Sure, sorry for the explanation, now that I read through it again I'm also having a hard time understanding it :sweat_smile:

Maybe a bit of context will help. I'm developing a Redux-like store architecture, much like The Composable Architecture. There are two ways to share the "store" between views. Either you make it an environment object, like in Recombine, or you pass it down in between each view using @ObservedObject, like in the aforementioned Composable Architecture. Each one of these methods has its own issues. Using the environment means that all the views will make a diff every single time some state changes, and passing down the store in between each view means tons of boilerplate, heavy view initialisation, and if you don't scope/lens the state yourself you'll also end up forcing a diff on all the views.

With my architecture I want the least boilerplate possible with the best performance possible. That's where the @Store property wrapper comes in – you just input the state your view wants in a keypath, and the store will be automatically scoped/lensed, so that your view only refreshes when meaningful state changes.

Here's some code of what I'd like to achieve:

@propertyWrapper struct Store<State, Action, LocalState>: DynamicProperty {
    @EnvironmentObject private var store: CentralStore<State, Action>
    
    var wrappedValue: LocalStore<LocalState, Action>
    
    private var cancellable: AnyCancellable?
    
    init(state: KeyPath<CentralStore.State, LocalState>) {
        cancellable = store
            .objectWillChange // Custom publisher which carries type metadata about which state will change
            .cancel(when: { typeMetadata in !typeMetadata.isContainedIn(state) }) // cancel the publisher upstream when the type which changed isn't in the keypath, so that the view doesn't reload
            .sink { _ in }
        
        self.wrappedValue = LocalStore(lensing: store, stateKeyPath: state)
    }
}

Here is my issue: DynamicProperty makes the view reload whenever the @EnvironmentObject store property receives an objectWillChange signal. Thus, a possible solution could be to stop the objectWillChange publisher from ever reaching the store property when the types don't match up.

I'm simply unsure how this could be done. I've explored making the objectWillChange a ConnectablePublisher, but that won't work since I'd still want the objectWillChange to publish to other views which might be interested in the state change, and anyways you can only connect()/cancel() once.

So I basically need a way to position myself in between the objectWillChange publisher and the @EnvironmentObject property wrapper, to block it whenever the state update doesn't line up with the given keypath.

I'd be grateful for any ideas!

See Optimizing views in SwiftUI using EquatableView | Swift with Majid

I don't know what it means for a publisher to "reach" a property.

It would be nice if you provide a self-contained example that I can compile.

I think I now understand the crux of your issue: You want a DynamicProperty that only causes a view to update when a certain property of the store environment object changes. Correct?

This is not possible. The ObservableObject decides when to send a signal from its objectWillChange publisher, and the EnvironmentObject property wrapper listens to any signals from this publisher. You can't "block" publishers. They either emit or they don't.

Correct, that's absolutely it. DynamicProperty seems even more mysterious than EnvironmentObject, which is why I wanted to try playing around with the second one first. However, locally blocking the objectWillChange seems impossible, so I may have to go a level higher, and try playing around with DynamicProperty itself.

I can't use an EquatableView which only allows an update of the child view when certain state changes, because then the user wouldn't be able to have any other properties or internal state in their view (which is too limiting IMO).

I was playing around with DynamicProperty too, and, indeed, it's very magical. If the EnvironmentObject (or any other DynamicProperty) inside your custom dynamic property changes, then that causes the view that uses your custom dynamic property to change as well. It's very unfortunate that SwiftUI doesn't seem to provide a hook for customizing when your DynamicProperty causes the view to update.

Agreed, that would enable incredibly expressive libraries. My intuition, and @Max_Desiatov 's Tokamak's implementation, tells me that they use a simple Mirror to recursively check which properties of the view conform to DynamicProperty, and I guess DynamicProperty does the same (maybe using memcmp).

Terms of Service

Privacy Policy

Cookie Policy