'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 |