Working with @propertyWrapper state

Hi all!

Does anyone know how to work with a @propertyWrapper state?

I'm experiencing an issue where the View doesn't get updated, even though the state is correctly updated (confirmed by logs).

struct SomeState: Equatable {
    @UserDefaultsBacked(.name) var name: String?
}

The full implementation of UserDefaultsBacked wrapper can be seen here.

The value of the name property is correctly updated, but its change is not reflected in the UI.

Any ideas? Thanks.

As this property wrapper communicates with outside world, you should not use it in state. Use Effects for that kind of stuff instead.

Another downside of using this kind of property wrappers is that you can update value in user defaults directly, but this change will be not propagated to the store, and thus will not invoke views rerender. Actually it kind-of mutates state, outside of reducer, which is not allowed.

1 Like

Thanks, @mpsnp. I 100% agree. However, even if the act of setting persisted values is moved to the environment, using Effect, there's still an open question of how to load persisted values at the time a container State is initialized.

Consider the following example:

struct SomeState: Equatable {
    // How do we load the initial value upon initialization?
	var persistedValue: Int
}

enum SomeAction: Equatable {
	case setPersistedValue(Int)
}

struct SomeEnvironment {
	var persistenceClient: Client
}

let someReducer = Reducer<SomeState, SomeAction, SomeEnvironment> { state, action, environment in
	switch action {
	case .setPersistedValue(let value):
		return environment.persistenceClient
			.set(value, for: "someKey")
			.eraseToEffect()
	}
}

One option I can think of is sending an action to the store on the onAppear event, which would also use the environment to fetch and set persistedValue's initial value. However, that might be too late, and would also force the value type to be optional, to allow for this kind of "lazy-loading".

Any ideas? Thanks.

@damirstuhec The idea is quite simple: if it can be too late to load the value inside the current state, then it's obvious, that this loading should occur in previous one (imagine that you are doing a network call :wink:). Here is a bunch of examples in TCA repo of how to load data before transition / after.

But maybe it's actually ok to load value in the current state? Just represent somehow the loading state in UI: loader or just disabled control, as this loading will occur in a fraction of a second, user will not even notice it.

In the end, checking out your original post, the value is optional anyway, as it is user defaults:

@UserDefaultsBacked(.name) var name: String?

Here is my attempt at the same problem:

import ComposableArchitecture
import Foundation

extension Reducer {
    static func strict(
        _ reducer: @escaping (inout State, Action) -> (Environment) -> Effect<Action, Never>
    ) -> Reducer {
        Self { state, action, environment in
            reducer(&state, action)(environment)
        }
    }
}

public struct AppState: Equatable {
    var isEnabled: Bool
}

public enum AppAction: Equatable {
    case onAppear
    case onDisappear
    case isEnabled(Bool)
}

public struct AppEnvironment {
    public var mainQueue: AnySchedulerOf<DispatchQueue>
    public var isEnabled: () -> Bool
    public var setIsEnabled: (Bool) -> Void

    public init(
        mainQueue: AnySchedulerOf<DispatchQueue>,
        isEnabled: @escaping () -> Bool,
        setIsEnabled: @escaping (Bool) -> Void
    ) {
        self.mainQueue = mainQueue
        self.isEnabled = isEnabled
        self.setIsEnabled = setIsEnabled
    }
}

public let appReducer =
    Reducer<AppState, AppAction, AppEnvironment>.strict { state, action in
        switch action {
        case .onAppear:
            return { environment in
              Effect(value: .isEnabled(environment.isEnabled()))
            }
        case .onDisappear:
            return { _ in .none }
        case .isEnabled(let flag):
            state.isEnabled = flag

            return { environment in
                guard environment.isEnabled() != flag else {
                    return .none
                }
                return .fireAndForget {
                    environment.setIsEnabled(flag)
                }
            }
        }
}

Am I doing this right?

This amounts to a lot amount of boiler plate just to be able to get and set UserDefaults and that's just for one variable.

Why don’t you just initialise your default app state when the app launches using values from UserDefaults before you load it into the store?

As convenient as something like a property wrapper is for something like this, they don’t really work in this architecture as they are effectively hidden side effects. You’ll want to make these explicit.

Loading the initial data is something you can do when you create your state in the app or scene (delegate) if it should be present before the view lifecycle begins. It’s effectively dependency bootstrapping. I would only handle data loading in a reduced if it’s something that can happen interactively or in response to some external event after the store is initialised.

these Settings can be changed or toggled within the app.

It seemed simpler to use the reducer. Especially for child states that get initialised at runtime: the parent would have to know too much about the child states just to initialised them.

I also didn’t want to use property wrappers because they hide what is happening and have gotchas.

I actually like that TCA doesn’t abuse properly wrappers.

@mycroftcanner how did this approach scale for you? I've got a similar scenario where I have several properties stored in UserDefaults that my view needs to be able to read and write. Did you find a way around defining a pair of getters/setters in your environment for each stored property?

@stephencelis @mbrandonw is there a "story" around this kind of interaction with the outside world? I've been looking through the example apps and case studies and can't find anything analogous to this (but I may be misunderstanding the problem and thus am missing a solution that's right in front of me.)

To break down the problem (just thinking out loud to see if it helps):

  1. Some global state exists in the outside world (a value in UserDefaults)
  2. The view needs to read and write access to this value
  3. The view can't (and shouldn't) access the environment directly, so it has to read the value from its view state.
  4. The initial view state must be created with the value currently stored in UserDefaults so it has to be retrieved before the view itself is created, and hopefully retrieved through the environment. This is where the above strict reducer is handy.
  5. The view also needs to write a different value to UserDefaults, via an action sent to the store -> reducer -> environment -> UserDefaults.
  6. Here's the big question in my mind:
    • At this point, does the reducer update the value in UserDefaults and update its own state (thus potentially creating two separate sources of truth) or should the reducer set the value in UserDefaults through the environment and then update the state by retrieving the value that was just set? If so, at scale, this becomes a fairly cumbersome dance and requires some sort of getter/setter pair for every property. Saying that though sets off some flags just from watching your videos — a getter/setter pair is extremely common. It could be a key path, a binding, something else? Any insights here?

If I'm right and there isn't an example or case study then this would be a great one to add. If I'm wrong, please point me to one? Thanks

3 Likes
Terms of Service

Privacy Policy

Cookie Policy