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.
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".
@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 ). 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:
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):
Some global state exists in the outside world (a value in UserDefaults)
The view needs to read and write access to this value
The view can't (and shouldn't) access the environment directly, so it has to read the value from its view state.
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.
The view also needs to write a different value to UserDefaults, via an action sent to the store -> reducer -> environment -> UserDefaults.
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