1

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.

Perry Hoekstra
  • 2,687
  • 3
  • 33
  • 52

4 Answers4

2

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.

Salim
  • 85
  • 11
  • 1
    I find that `@InjectedObject` only works when on a `View`, so unfortunately the example you have here doesn't work for me unless I observe a given property from `LoginViewModel` and manually update with `objectWillChange` – lacking-cypher May 28 '22 at 02:56
1

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

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • If you use a `DynamicProperty` struct instead of a view model object (We don't use objects for view data in SwiftUI), you can use `@Environment` within it and it is valid in the `update` method. That way you don't even need the apply hack. An example of Apple using this is the FetchRequest struct that accesses the managedObjectContext environment value in its update func. – malhal Jul 19 '22 at 11:08
-1

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.

You can group your related vars into their own custom struct and use mutating funcs to manipulate them. You can even use @Environment vars if you conform the struct to DynamicProperty and read them in the update func which is called on the struct before the View's body. You can even use a @StateObject if you need a reference type, e.g. to use as an NSObject's delegate.

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.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • In regards to the statement "MVVM has no place in SwiftUI", others would disagree such as https://medium.com/macoclock/swiftui-mvvm-clean-architecture-e976ad3577b5 – Perry Hoekstra Mar 15 '22 at 00:14
  • Annoying that blogger didn’t bother to learn SwiftUI – malhal Mar 15 '22 at 09:24
  • I don't see a reason that "StateObject"s should not have dependencies. These dependencies may be pure, or may be "shared state" and may perform side effects, which then should be actors in order to make the system work correctly in a concurrent environment (which we usually have in an iOS app). You may take a look at my answer which describes an approach fully aligned with SwiftUI. If we want to, we could remove the ViewModel and implement the functionality into the view. But this will lead to "massive SwiftUI" views. So why not use "data focussed components" (avoiding the word ViewModel) ? – CouchDeveloper Mar 22 '22 at 17:52
  • Related vars and functionality (that you might want to test) can be moved into another struct. Moving it to a view model object just for these reasons is the wrong approach. There is info on why choose structs over classes here https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes – malhal Mar 22 '22 at 20:15
-1

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())
    }
}
azamsharp
  • 19,710
  • 36
  • 144
  • 222
  • FooView(vm: FooViewModel()) is what I was trying to avoid with Resolver. This way, you could use FooView() and Resolver would instantiate FooViewModel. – Perry Hoekstra Mar 15 '22 at 11:51
  • Initing an heap object within body is a mistake, it needs to be done in the @StateObject declaration – malhal Mar 22 '22 at 20:23