'Inject a StateObject into SwiftUI View

Can @StateObject be injected using Resolver?

I have the following:

struct FooView: View {
    @StateObject private var viewModel: FooViewModel

    some code
}
protocol FooViewModel: ObservableObject {
    var someValue: String { get }
    func someRequest()
}

class FooViewModelImpl {
   some code
}

I would like to inject FooViewModel into FooView using Resolver but have been struggling as Resolver wants to use the @Inject annotation and of course, I need the @StateObject annotation but I cannot seem to use both. Are @StateObject not able to be injected using some Dependency Injection framework like Resolver? I have not found any examples where developers have used DI in this approach.



Solution 1:[1]

The latest version of Resolver supports @InjectedObject property wrapper for ObservableObjects. This wrapper is meant for use in SwiftUI Views and exposes bindable objects similar to that of SwiftUI @ObservedObject and @EnvironmentObject.

I am using it a lot now and its very cool feature.

eg:

class AuthService: ObservableObject {

    @Published var isValidated = false

}

class LoginViewModel: ObservableObject {

    @InjectedObject var authService: AuthService

}

Note: Dependent service must be of type ObservableObject. Updating object state will trigger view update.

Solution 2:[2]

If your StateObject has a dependency - and instead to utilise a heavy weight Dependency Injection Framework - you could utilise Swift Environment and a super light wight "Reader Monad" to setup your dependency injected state object, and basically achieve the same, just with a few lines of code.

The following approach avoids the "hack" to setup a StateObject within the body function, which may lead to unexpected behaviour of the StateObject. The dependent object will be fully initialised once and only once with a default initialiser, when the view will be created. The dependency injection happens later, when a function of the dependent object will be used:

Given a concrete dependency, say SecureStore conforming to a Protocol, say SecureStorage:

extension SecureStore: SecureStorage {}

Define the Environment Key and setup the default concrete "SecureStore":

private struct SecureStoreKey: EnvironmentKey {
    static let defaultValue: SecureStorage =
        SecureStore(
            accessGroup: "myAccessGroup"
            accessible: .whenPasscodeSetThisDeviceOnly
        )
}

extension EnvironmentValues {
    var secureStore: SecureStorage {
        get { self[SecureStoreKey.self] }
        set { self[SecureStoreKey.self] = newValue }
    }
}

Elsewhere, you have a view showing some credential from the secure store, which access will be handled by the view model, which is setup as a @StateObject:

struct CredentialView: View {
    @Environment(\.secureStore) private var secureStore: SecureStorage
    @StateObject private var viewModel = CredentialViewModel()
    @State private var username: String = "test"
    @State private var password: String = "test"

    var body: some View {
        Form {
            Section(header: Text("Credentials")) {
                TextField("Username", text: $username)
                    .keyboardType(.default)
                    .autocapitalization(.none)
                    .disableAutocorrection(true)

                SecureField("Password", text: $password)
            }
            Section {
                Button(action: {
                    self.viewModel.send(.submit(
                        username: username,
                        password: password
                    ))
                    .apply(e: secureStore)
                }, label: {
                    Text("Submitt")
                    .frame(minWidth: 0, maxWidth: .infinity)
                })
            }
        }   
        .onAppear {
            self.viewModel.send(.readCredential)
                .apply(e: secureStore)
        }
        .onReceive(self.viewModel.$viewState) { viewState in
            print("onChange: new: \(viewState.credential)")
            username = viewState.credential.username
            password = viewState.credential.password
        }
    }
}

The interesting part here is where and when to perform the dependency injection:

   self.viewModel.send(.submit(...))
       .apply(e: secureStore) // apply the dependency

Here, the dependency "secureStore" will be injected into the view model in the action function of the Button within the body function, utilising the a "Reader", aka .apply(environment: <dependency>).

Note also that the ViewModel provides a function

send(_ Event:) -> Reader<SecureStorage, Void>

where Event just is an Enum which has cases for every possible User Intent.

final class CredentialViewModel: ObservableObject {
    struct ViewState: Equatable {
        var credential: Credential = 
           .init(username: "", password: "")
    }
    enum Event {
        case submit(username: String, password: String)
        case readCredential
        case deleteCredential
        case confirmAlert
    }

    @Published var viewState: ViewState = .init()

    func send(_ event: Event) -> Reader<SecureStorage, Void>
    ...

Your View Model can then implement the send(_:) function as follows:

func send(_ event: Event) -> Reader<SecureStorage, Void> {
    Reader { secureStore in
        switch event {
        case .readCredential:
            ...
        case .submit(let username, let password):
            secureStore.set(
                item: Credential(
                    username: username,
                    password: password
                ),
                key: "credential"
            )
        case .deleteCredential:
            ... 
    }
}

Note how the "Reader" will be setup. Basically quite easy: A Reader just holds a function: (E) -> A, where E is the dependency and A the result of the function (here Void).

The Reader pattern may be mind boggling at first. However, just think of send(_:) returns a function (E) -> Void where E is the secure store dependency, and the function then just doing whatever was needed to do when having the dependency. In fact, the "poor man" reader would just return this function, just not a "Monad". Being a Monad opens the opportunity to compose the Reader in various cool ways.

Minimal Reader Monad:

struct Reader<E, A> {
    let g: (E) -> A
    init(g: @escaping (E) -> A) {
        self.g = g
    }
    func apply(e: E) -> A {
        return g(e)
    }
    func map<B>(f: @escaping (A) -> B) -> Reader<E, B> {
        return Reader<E, B>{ e in f(self.g(e)) }
    }
    func flatMap<B>(f: @escaping (A) -> Reader<E, B>) -> Reader<E, B> {
        return Reader<E, B>{ e in f(self.g(e)).g(e) }
    }
}

For further information about the Reader Monad: https://medium.com/@foolonhill/techniques-for-a-functional-dependency-injection-in-swift-b9a6143634ab

Solution 3:[3]

No, @StateObject is for a separate source of truth it shouldn't have any other dependency. To pass in an object, e.g. the object that manages the lifetime of the model structs, you can use @ObservedObject or @EnvironmentObject.

FYI we don't use view models objects in SwiftUI. See this answer "MVVM has no place in SwiftUI."

ObservableObject is part of the Combine framework so you usually only use it when you want to assign the output of a Combine pipeline to an @Published property. Most of the time in SwiftUI and Swift you should be using value types like structs. See Choosing Between Structures and Classes. We use DynamicProperty and property wrappers like @State and @Binding to make our structs behave like objects.

Solution 4:[4]

Not sure about resolver but you can pass VM to a V using the following approach.

import SwiftUI

class FooViewModel: ObservableObject {
    @Published var counter: Int = 0
}

struct FooView: View {
    
    @StateObject var vm: FooViewModel
    
    var body: some View {
        VStack {
            Button {
                vm.counter += 1
            } label: {
                Text("Increment")
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        FooView(vm: FooViewModel())
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Salim
Solution 2
Solution 3
Solution 4 azamsharp