Updating Dependencies at a later point in the app's lifecycle #2935
Replies: 4 comments 12 replies
-
I think Maintainer gave similar advice in the Slack the other day:
|
Beta Was this translation helpful? Give feedback.
-
Hi @LucasVanDongen, thanks for taking the time to create a project with many different styles of dependencies. However, your project does not currently compile due to a missing Package.swift. Can you update it so that it compiles? |
Beta Was this translation helpful? Give feedback.
-
I've got it working like this: struct Features: Reducer {
@ObservableState
enum State {
case logIn(LogInFeature.State)
case authenticated(AuthenticatedFeature.State)
}
enum Action {
case logIn(LogInFeature.Action)
case authenticated(AuthenticatedFeature.Action)
}
var body: some Reducer<State, Action> {
CombineReducers {
Reduce { state, action in
switch action {
case let .logIn(.authenticatedSuccessfully(token)):
state = .authenticated(AuthenticatedFeature.State(token: token))
return .none
case .logIn:
return .none // Ignore the rest
case let .authenticated(.authenticated(token)):
state = .authenticated(AuthenticatedFeature.State(token: token))
return .none
case .authenticated:
return .none
}
}
ReducerReader(reader: { state, action in
switch state {
case .logIn:
Scope(state: /State.logIn, action: /Action.logIn) {
let store = Store(initialState: state) {
self
}
LogInFeature(parentStore: store)
}
case let .authenticated(state):
Scope(state: /State.authenticated, action: /Action.authenticated) {
AuthenticatedFeature()
.dependency(\.userManager, UserManager(token: state.token))
.dependency(\.storyFetcher, StoryFetcher(token: state.token))
}
}
})
}
}
} However, the only feature that has these overwritten values is
So I see the following in may app when I run it: ...but I'm going to take a break from this and take a fresh breath. The progress up to this point is in the following branch / commit: LucasVanDongen/SwiftDependencyInjectionCompared@82f2e10 |
Beta Was this translation helpful? Give feedback.
-
Hey @LucasVanDongen, thanks again for taking the time to put this project together. It definitely helps us understand how you are approaching dependencies in a variety of ways. There is a lot to respond here, so it's tough to know where to start. Let me start with some basics: First, I really like that you have built the same tiny app using a variety of dependency styles. This makes it easy for people to study the differences and even contribute their own styles. However, the project still doesn't really compile cleanly on its own. I would recommend making all the projects iOS instead of macOS so that people don't have to mess with app signing, the Package.swift needs to target iOS 17+, and I recommend using https for git packages rather than SSH (it's more portable). Also, the needle project still doesn't build. I needed to comment out the Second, you mentioned previously that a goal is to have the concept of a dependency that is somehow unavailable until the user authenticates. I believe that is what the However, most of the projects don't actually demonstrate that capability. You can create The only ones that do have this behavior are "PassedDependencies" and NeedleDependencies", but in each of those projects the dependencies are just being passed around explicitly. I must admit, I don't really understand what needle is accomplishing in this project. It seems to be quite a bit work, and doesn't play nicely with Swift concurrency warnings or SwiftUI. And finally, I have rebuilt your demo app using just our swift-dependencies library and vanilla SwiftUI. I'm not using TCA at all because it's really not necessary to get the benefits of dependencies. Here's the diff with your I tried my hardest to keep the overall app structure true to what you have in the other apps. So I didn't change the dependencies at all, or the logic in the features, but I do think there is more room for simplifications and improvements. There are some interesting things in this new SwiftDepedencies app target, and a few of the problems pointed out in your comments have actually been fixed. For example, in the environment style of dependencies you run the risk of forgetting to set up dependencies in the view hierarchy, resulting either in the wrong dependency being silently used, or a loud crash. In particular, you need to make sure to do this: AuthenticatedView()
.environment(\.userManager, UserManager(token: token))
.environment(StoryFetcher(token: token)) …when the app flips to an authenticated state. But also even this isn't ideal, because this only works (as you noted) for stateless dependencies, which is probably not super common. This is solved in the SwiftDependencies project by creating the state = .authenticated(
withDependencies(from: self) {
$0.userManager = UserManager(token: token)
$0.storyFetcher = StoryFetcher(token: token)
} operation: {
AuthenticatedModel()
}
) This statement runs only a single time, at the moment the app switches to authenticated mode. It doesn't matter how many times the view re-draws itself. Further, these dependency changes are propagated deeply into the app but in a tree-like fashion. The dependency overrides are only active for As another example of a downside, in the Factory demo you have the following comment:
…with regards to these lines of code: // First assign the dependencies
AuthenticatedDependenciesManager.handle(token: token)
// Then flip the UI switch
state = switch token.isEmpty {
case true: .loggedOut
case false: .authenticated(token: token)
} And this definitely is subtle. The SwiftDependencies project also fixes this problem, and with the same lines I pasted above: state = .authenticated(
withDependencies(from: self) {
$0.userManager = UserManager(token: token)
$0.storyFetcher = StoryFetcher(token: token)
} operation: {
AuthenticatedModel()
}
) This simultaneously populates the state to drive the navigation in the view and overrides dependencies for that child feature. There is no two-step process to get out of order because there is no global, mutable singleton. Everything is held in a Next you have a few comments about the various "placeholder" dependencies you have: // This implementation is used to prevent a `nil` value for this dependency while the user is not authenticated yet
…
// You have an issue if this function would actually be called
…
// If this function would ever be executed, we have a problem And these comments are just purely aspirational right now. It is completely possible to accidentally use these placeholder dependencies in the environment and Factory projects. Our swift-dependencies library also solves this problem. When registering the dependency with the library you can provide only a struct TestStoryFetcher: StoryFetching {
func fetchStories() async throws -> [Story] {
return []
}
}
private enum StoryFetcherKey: TestDependencyKey {
static let testValue: any StoryFetching = TestStoryFetcher()
} However, crucially, if this dependency is accessed in a "live" context (e.g. simulator or device), then a runtime warning will be emitted in Xcode letting you know that the dependency must be overridden. For example, once the user is authenticated, what if we forgot to override the state = .authenticated(
// withDependencies(from: self) {
// $0.userManager = UserManager(token: token)
// $0.storyFetcher = StoryFetcher(token: token)
// } operation: {
AuthenticatedModel()
// }
) Go ahead and try it out in my fork. You will find that once you get to the authenticated screen you immediately get purple runtime warnings in Xcode letting you know that the dependencies have not been overridden: It's worth comparing this to your environment-based project, which will crash if you don't override the dependencies, or the Factory-based project, which will use a placeholder dependency with no warning. And it technically is possible to structure dependencies in a way with our library that allows you to gain access to a dependency only after the user authenaticates, but it doesn't come for free (just as it doesn't come for free with the PassedDependencies or NeedleDependencies projects). We should get some documentation on this technique so that it can be publicly known. Sorry this post was so long, but hopefully it helps a bit. And I specifically did not want to talk about TCA because I feel the discussion is about dependencies in general, and really doesn't have anything to do with TCA. However, all of the ideas mentioned above do work with TCA just fine. I did look at your branch a bit, but I can say that you definitely should not approach the problem that way. You should never hold onto a |
Beta Was this translation helpful? Give feedback.
-
I'm investigating TCA in general and its dependency management in general. In general everything clicked together pretty well but I have trouble switching from log in to authenticated state. The authenticated state has several dependencies that can only work when a token is present.
While I understand there are ways to work around it, like updating a static dependency with the token and clearing it out again when logging out I would like to figure out if it's possible to update the dependency later on, so they will automatically disappear again when the AuthenticatedFeature ends when the user logs out. There are also other more complex scenarios imaginable where you really want to change behavior of a dependency depending on where you are in the application.
According to the docs I should use
.dependency
to change them, but withifCaseLet
I cannot access that token:I tried using the yet-to-be-merged
ReducerReader
feature, but I got stuck doing the implementation as the branch seems to be outdated and the examples were not complete working examples.I think it was the way though.
The whole repo can be found here
Can somebody lend me a hand with this?
Beta Was this translation helpful? Give feedback.
All reactions