11

Let's say I have a view model called AuthViewModel which handles all authentication-related activities and states of my SwiftUI app, and it requires the following dependencies:

  • A HTTPClient object to run HTTP requests (this is an abstraction over the URLSession logic)
  • A SecureStorage object to abstract keychain access logic for secrets
  • A CoreDataStorage object to abstract Core Data access logic

These classes should also ideally be passed down the view hierarchy so that other view models could reuse them too.

The problem is I've been trying different ways to inject these dependencies that are required in the init method of my AuthViewModel without success. You can imagine the init method looks like this:

class AuthViewModel: ObservableObject {
  @Published private(set) var authToken: String?
  
  init(httpClient: HTTPClient, secureStorage: SecureStorage, coreDataStorage: CoreDataStorage) {
  }
}

Here are a few things that I have tried and/or found unsatisfactory:

1. Turn all the dependencies listed above into ObservableObjects and use .environmentObject to pass them down the view hierarchy

This doesn't work because if I pluck off the @EnvironmentObject in the same View body that defines the authViewModel, the init method of the view model doesn't have access to self, and hence the rest of the environmentally-injected dependencies.

struct LoginView: View {
  @EnvironmentObject private var httpClient: HTTPClient
  @EnvironmentObject private var secureStorage: SecureStorage
  @EnvironmentObject private var coreDataStorage: CoreDataStorage
  
  // This won't work because at the current scope `self` is not ready
  @StateObject private var authViewModel = AuthViewModel(
    httpClient: httpClient,
    secureStorage: secureStorage,
    coreDataStorage: coreDataStorage
  )
}

2. Remove dependencies from the initializer and move them into settable properties

This is a plausible workaround but it generates too much work unwrapping the optional dependencies for its internal methods, not to mention that these dependencies are required for the functioning of the view model and should really have non-null values at all times. In a large application, we must also ensure that other developers do not forget to set the dependencies—if only there was a failsafe way of helping them not forget about required dependencies am I right?

3. Make the dependencies private environment properties within the view model itself

This defeats the purpose of Dependency Injection, because it's turning our view model from a blackbox into a whitebox object, meaning that we cannot successfully operate this object without explicit knowledge of its internals, aka the private environment dependencies inside of it. This complicates testing and requires the developer to always look inside of the private properties of the view model to make use of it properly.

Also, this is almost akin to using singletons of dependencies, which is the very thing we are trying to avoid here. At least, I'd like these singletons to be in the View body, but once we cross over into the view model's territory I'd like them to be injected at the very least.

4. Inject these dependencies at the top level of the application where the dependencies are defined:

There were suggestions floating around to define these outside the scope of the @main struct.

private let httpClient = ...
private let secureStorage = ...
private let coreDataStorage = ...
private let authViewModel = AuthViewModel(httpClient: httpClient, secureStorage: secureStorage, coreDataStorage: coreDataStorage)

@main
struct MyApp: App {
  @StateObject private var myAuthViewModel = authViewModel
  
  var body: some Scene {
    WindowScene {
      SplashScreen()
        .environmentObject(myAuthViewModel)
    }
  }
}

This unconventional approach has one downside: I don't just have this one view model in my app. There might be other view models down the line that requires the dependencies too, and there's no initializing them like this then.


To solve this, I am seeking a solution that:

  • Allows me to define a few dependencies at the @main struct of my app and pass them down such that other view models can be initialized with them
  • Does not sacrifice the testability of the view model, and maintains its decoupled nature
  • Allow multiple instances of the same dependency to co-exist. For example, I might want to have different HTTPClients for API requests and for downloading/uploading operations

PS: At the time of writing Xcode 13 is still in beta, but if there is a solution that requires Swift 5.5 I'm all ears. I'm using it anyway.

11
  • Check out github.com/Liftric/DIKit Commented Aug 24, 2021 at 2:07
  • Your problem is you are trying to create the @StateObject inside the consuming view. As you have found this doesn't work. You need to create the @StateObject in the view's parent and then either pass it via the environment or as an initialiser parameter to the view Commented Aug 24, 2021 at 2:09
  • 1
    @Paulw11 Thanks for the suggestion, that indeed will work. However, the problem is that the view's parent will also need to get these dependencies somehow, and usually a view's parent is another view. How do I construct a @StateObject in this parent that will also require these dependencies? Commented Aug 24, 2021 at 4:55
  • It's turtles all the way down; You inject the parent's view model from its parent and so on. You put the dependencies in the environment as per point 1 in your question (or you can hold them as properties of a single object you put in the environment if you like) Commented Aug 24, 2021 at 4:59
  • 1
    Does this answer your question stackoverflow.com/a/62636048/12299030? Please note: it does not fit all scenarios - read comments. Commented Aug 24, 2021 at 5:22

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.