【问题标题】:Inject a StateObject into SwiftUI View将 StateObject 注入 SwiftUI 视图
【发布时间】:2022-07-19 19:11:57
【问题描述】:

@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 的示例。

【问题讨论】:

  • Resolver 仅用于 UIKit 吗?

标签: swift swiftui resolver


【解决方案1】:

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

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

例如:

class AuthService: ObservableObject {

    @Published var isValidated = false

}

class LoginViewModel: ObservableObject {

    @InjectedObject var authService: AuthService

}

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

【讨论】:

  • 我发现@InjectedObject 仅在View 上有效,所以不幸的是,除非我观察到来自LoginViewModel 的给定属性并手动更新,否则您在此处的示例对我不起作用objectWillChange
【解决方案2】:

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

以下方法避免了在 body 函数中设置 StateObject 的“hack”,这可能导致 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”将被注入到视图模型中的主体函数内按钮的动作函数中,利用“阅读器”,又名.apply(environment: <dependency>)

还要注意 ViewModel 提供了一个函数

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

其中Event 只是一个Enum,它为每个可能的用户意图提供了案例。

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 只保存一个函数:(E) -&gt; A,其中E 是依赖项,A 是函数的结果(此处为 Void)。

Reader 模式起初可能令人难以置信。但是,想想send(_:) 返回一个函数(E) -&gt; Void,其中 E 是安全存储依赖项,然后该函数只是在有依赖项时做任何需要做的事情。事实上,“穷人”阅读器只会返回这个函数,而不是“Monad”。成为 Monad 为以各种酷炫的方式编写 Reader 提供了机会。

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

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

【讨论】:

  • 如果你使用 DynamicProperty 结构而不是视图模型对象(我们在 SwiftUI 中不使用对象作为视图数据),你可以在其中使用 @Environment 并且它在update 方法。这样你甚至不需要 apply hack。 Apple 使用它的一个例子是 FetchRequest 结构,它在其更新函数中访问 managedObjectContext 环境值。
【解决方案3】:

不确定解析器,但您可以使用以下方法将 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())
    }
}

【讨论】:

  • FooView(vm: FooViewModel()) 是我试图通过 Resolver 避免的。这样,您可以使用 FooView() 并且 Resolver 将实例化 FooViewModel。
  • 在body中初始化堆对象是错误的,需要在@StateObject声明中进行
【解决方案4】:

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

您可以将相关的变量分组到它们自己的结构中,并使用变异函数来操作它们。你甚至可以使用@Environment vars,如果你使结构符合DynamicProperty 并在update 函数中读取它们,该函数在View 的主体之前在结构上调用。

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

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

【讨论】:

  • 关于“MVVM 在 SwiftUI 中没有立足之地”的说法,其他人会不同意,例如 medium.com/macoclock/…
  • 讨厌博主懒得学 SwiftUI
  • 我看不出“StateObject”不应该有依赖关系的原因。这些依赖可能是纯粹的,也可能是“共享状态”并且可能执行副作用,然后它们应该是参与者,以使系统在并发环境中正常工作(我们通常在 iOS 应用程序中拥有)。你可以看看我的回答,它描述了一种与 SwiftUI 完全一致的方法。如果我们愿意,我们可以移除 ViewModel 并将功能实现到视图中。但这将导致“大量 SwiftUI”视图。那么为什么不使用“以数据为中心的组件”(避免使用 ViewModel 这个词)?
  • 相关的变量和功能(您可能想要测试)可以移动到另一个结构中。仅出于这些原因将其移动到视图模型对象是错误的方法。这里有关于为什么选择结构而不是类的信息developer.apple.com/documentation/swift/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-20
  • 2022-11-16
  • 1970-01-01
  • 2020-07-15
相关资源
最近更新 更多