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.

Terms of Service

Privacy Policy

Cookie Policy