Iāve just come up against a sharp edge in using swift-dependencies which others may find useful to know, if itās not obvious (it wasnāt to me at first). Itās not a direct issue with swift-dependencies, but something that happens naturally if youāre using the library and using something along the lines of Point-Freeās 'tree of view models' setup for an app.
Given a dependency with a long-running publisher endpoint:
public struct Authentication {
// [...]
public var updates: AnyPublisher<User?, Never>
}
Naturally youāre going to subscribe to that in your view models to keep any user-related state current:
@MainActor public final class ProfileModel: ObservableObject {
@Dependency(\.authentication) var authentication
@Published var isSignedIn = false
public init() {
Task {
for await user in authentication.updates.values {
isSignedIn = user != nil
}
}
}
deinit {
print("Iām called when the profile screen is dismissed, right?")
}
}
In fact, the profile model never gets released when its parent model changes destination it because thereās a retain cycle in the task here, which implicitly captures self through authentication and isSignedIn.
You can do one of two things. You can start adding capture groups to all your tasks:
Task { [weak self, authentication] in
for await user in authentication.updates.values {
self?.isSignedIn = user != nil
}
}
Personally I donāt like this because itās so easy to miss the self?. on a property inside the task.
The other option which makes a lot more sense is to use structured concurrency by adding SwiftUIās task {} modifier to take care of the lifetime of the task. You can then capture self as much as you like and it wonāt matter because thereās a definite point at runtime where the task will be cancelled and all your self references go away safely before deinit.
// ProfileModel.swift
// [...]
func trackUserUpdates() async {
for await user in authentication.updates.values {
isSignedIn = user != nil
}
}
// ProfileScreen.swift
// [...]
var body: some View {
Text("Profile Screen")
.task { await model.trackUserUpdates() }
}
This is probably super-obvious to many users, but it took me a while to notice the problem. The one thing Iām not sure about is that not starting the task automatically in init means that during testing creating an instance of the model doesnāt immediately set up all the expected state (notably if the underlying update publisher is using a CurrentValueSubject or equivalent so that your model would usually get an immediate initial value). You have to start and stop that task manually rather than it just coming along for the ride as the model is inited and released. Maybe thatās ok? Maybe having long-running task publishers in dependencies is an anti-pattern? But whatās the alternative for external things that change like auth states and data stores? Iād love to hear from anyone else working with this setup.