将 StateObject 注入 SwiftUI 视图

Inject a StateObject into SwiftUI View

@StateObject 可以使用 Resolver 注入吗?

我有以下内容:

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

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

class FooViewModelImpl {
   some code
}

我想使用 Resolver 将 FooViewModel 注入到 FooView 中,但一直在努力,因为 Resolver 想要使用 @Inject 注释,当然,我需要 @StateObject 注释,但我似乎无法同时使用两者。 @StateObject 是否无法使用 Resolver 等依赖注入框架进行注入?我还没有找到任何开发人员在这种方法中使用 DI 的示例。

不,@StateObject 是一个单独的事实来源,它不应该有任何其他依赖。传递一个对象,例如管理模型结构生命周期的对象,您可以使用 @ObservedObject@EnvironmentObject.

仅供参考,我们不在 SwiftUI 中使用视图模型对象。看到这个答案 "MVVM has no place in SwiftUI."

ObservableObject 是 Combine 框架的一部分,因此您通常只在需要将 Combine 管道的输出 assign 输出到 @Published 属性 时才使用它。在 SwiftUI 和 Swift 中,大多数时候你应该使用像结构这样的值类型。参见 Choosing Between Structures and Classes。我们使用 DynamicProperty 和 属性 包装器,如 @State@Binding 来使我们的结构表现得像对象。

不确定解析器,但您可以使用以下方法将 VM 传递给 V。

导入 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())
    }
}

如果你的 StateObject 有一个依赖项——而不是使用一个重量级的依赖注入框架——你可以利用 Swift 环境和一个超轻的“Reader Monad”来设置你的依赖注入state对象,基本实现一样,只是几行代码。

以下方法避免了在主体函数中设置 StateObject 的“黑客攻击”,这可能会导致 StateObject 出现意外行为。当创建视图时,依赖对象将使用默认初始化程序完全初始化一次且仅一次。依赖注入稍后发生,当依赖对象的函数将被使用时:

给定一个具体的依赖关系,说 SecureStore 遵守协议,说 SecureStorage:

extension SecureStore: SecureStorage {}

定义环境密钥并设置默认的具体“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 }
    }
}

在其他地方,您有一个显示来自安全存储的一些凭据的视图,该访问将由视图模型处理,该视图模型设置为 @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
        }
    }
}

这里有趣的部分是何时何地执行依赖注入:

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

在这里,依赖项“secureStore”将在 body 函数内的 Button 的操作函数中注入视图模型,利用“Reader”,又名 .apply(environment: <dependency>)

另请注意,ViewModel 提供了一个函数

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

其中 Event 只是一个 Enum,它包含每个可能的 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>
    ...

然后您的视图模型可以按如下方式实现 send(_:) 函数:

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:
            ... 
    }
}

请注意“Reader”的设置方式。基本上很容易: Reader 只包含一个函数:(E) -> A,其中 E 是依赖项,A 是函数的结果(这里是 Void)。

Reader 模式起初可能令人难以置信。但是,只要想一想 send(_:) returns 一个函数 (E) -> Void,其中 E 是安全存储依赖项,然后该函数只执行具有依赖项时需要执行的任何操作。事实上,“穷人”reader 只会return 这个函数,而不是“Monad”。成为 Monad 开启了以各种很酷的方式编写 Reader 的机会。

最小Reader单子:

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) }
    }
}

有关 Reader Monad 的更多信息: https://medium.com/@foolonhill/techniques-for-a-functional-dependency-injection-in-swift-b9a6143634ab

Resolver 的最新版本支持 @InjectedObject 属性 ObservableObjects 的包装器。此包装器旨在用于 SwiftUI 视图并公开类似于 SwiftUI @ObservedObject 和 @EnvironmentObject 的可绑定对象。

我现在经常使用它,它的功能非常棒。

例如:

class AuthService: ObservableObject {

    @Published var isValidated = false

}

class LoginViewModel: ObservableObject {

    @InjectedObject var authService: AuthService

}

注意:依赖服务必须是ObservableObject类型。更新对象状态将触发视图更新。