我是合并的新手,正在努力解决一些关于通信的概念。我来自网络背景,在此之前是UIKit,所以SwiftUI是一个不同的景观。
我非常热衷于使用MVVM
来保持业务逻辑远离View
层。这意味着任何不是可重用组件的视图都有一个ViewModel
来处理API请求,逻辑,错误处理等。
我遇到的问题是,当ViewModel
中发生了一些事情时,将事件传递给View
的最佳方式是什么。我理解视图应该是状态的反映,但对于事件驱动的事情,它需要一堆变量,我认为这很混乱,因此渴望获得其他方法。
下面的例子是一个ForgotPasswordView
。它以工作表的形式呈现,当成功重置时,它应该关闭+显示一个成功的吐司。在失败的情况下,应该显示一个错误的toast(对于上下文,全局toast协调器是通过在应用根目录注入的@Environment
变量来管理的)。
下面是一个有限的例子View
struct ForgotPasswordView: View {
/// Environment variable to dismiss the modal
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
/// The forgot password view model
@StateObject private var viewModel: ForgotPasswordViewModel = ForgotPasswordViewModel()
var body: some View {
NavigationView {
GeometryReader { geo in
ScrollView {
// Field contents + button that calls method
// in ViewModel to execute the network method. See `sink` method for response
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(leading: self.closeButton /* Button that fires `closeSheet` */)
}
}
/// Close the presented sheet
private func closeSheet() -> Void {
self.presentationMode.wrappedValue.dismiss()
}
}
ViewModel
class ForgotPasswordViewModel: ObservableObject {
/// The value of the username / email address field
@Published var username: String = ""
/// Reference to the reset password api
private var passwordApi = Api<Response<Success>>()
/// Reference to the password api for cancelling
private var apiCancellable: AnyCancellable?
init() {
self.apiCancellable = self.passwordApi.$status
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let result = result else { return }
switch result {
case .inProgress:
// Handle in progress
case let .success(response):
// Handle success
case let .failed(error):
// Handle failure
}
}
}
}
上面的ViewModel
包含了所有的逻辑,而View
只是简单地反映了数据和调用方法。到目前为止一切都很好。
现在,为了处理服务器响应的success
和failed
状态,并将该信息发送到UI,我遇到了问题。我可以想到一些方法,但我要么不喜欢,要么似乎不可能。
带有变量
为每个状态创建单独的@Published
变量,例如@Published var networkError: String? = nil
然后设置它们是不同的状态
case let .failed(error):
// Handle failure
self.networkError = error.description
}
然后,在View
中,我可以通过onRecieve
订阅此内容并处理响应
.onReceive(self.viewModel.$networkError, perform: { error in
if error {
// Call `closeSheet` and display toast
}
})
这是可行的,但这是一个单独的例子,需要我为每个状态创建一个@Published
变量。此外,这些变量也必须被清理(将它们设置回nil)。
如果使用一个enum
和相关的值,这样就只需要使用一个listener +变量,而enum并不需要处理这个变量需要被清理的事实。
带PassthroughSubject
在此基础上,我查看了PassthroughSubject
,认为如果创建一个类似
@Publisher var events: PassthoughSubject = PassthroughSubject<Event, Never>
并发布这样的事件:
.sink { [weak self] result in
guard let result = result else { return }
switch result {
case let .success(response):
// Do any processing of success response / call any methods
self.events.send(.passwordReset)
case let .failed(error):
// Do any processing of error response / call any methods
self.events.send(.apiError(error)
}
}
然后我就可以这样听了
.onReceive(self.viewModel.$events, perform: { event in
switch event {
case .passwordReset:
// close sheet and display success toast
case let .apiError(error):
// show error toast
})
这比变量更好,因为事件是用.send
发送的,所以events
变量不需要清理。
不幸的是,你似乎不能将onRecieve
与PassthroughSubject
一起使用。如果我将它作为一个Published
变量,但具有相同的概念,那么我将遇到第一个解决方案所具有的必须再次清理它的问题。
一切尽在眼前
最后一种情况,我一直试图避免的是处理View
中的所有内容
struct ForgotPasswordView: View {
/// Environment variable to dismiss the modal
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
/// Reference to the reset password api
@StateObject private var passwordApi = Api<Response<Success>>()
var body: some View {
NavigationView {
GeometryReader { geo in
ScrollView {
// Field contents + button that all are bound/call
// in the view.
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(leading: self.closeButton /* Button that fires `closeSheet` */)
.onReceive(self.passwordApi.$status, perform: { status in
guard let result = result else { return }
switch result {
case .inProgress:
// Handle in progress
case let .success(response):
// Handle success via closing dialog + showing toast
case let .failed(error):
// Handle failure via showing toast
}
})
}
}
}
上面是一个简单的例子,但如果需要进行更复杂的处理或数据操作,我不希望在View
中进行,因为它会很混乱。此外,在这种情况下,成功/失败事件与需要在UI中处理的事件完全匹配,但不是每个视图都属于该类别,因此可能需要进行更多的处理。
我遇到了这个难题,几乎每个视图都有一个模型,如果在ViewModel
中发生了一些基本事件,它应该如何传达到View
。我觉得应该有一个更好的方法来做到这一点,这也让我觉得我做错了。
这是一个很大的文本墙,但我热衷于确保应用程序的架构可维护,易于测试,视图专注于显示数据和调用变化(但不以ViewModel
中有大量样板变量为代价)
谢谢
1条答案
按热度按时间vwhgwdsa1#
您可以将重置密码请求的结果发送到视图模型的
@Published
属性。当状态更改时,SwiftUI将自动更新关联的视图。下面我写了一个类似于你的密码重置表单,有一个视图和一个底层的视图模型。视图模型有一个
state
,其中有四个可能的值来自嵌套的State
枚举:idle
作为初始状态或用户名已更改后。loading
当执行复位请求时。success
和failure
。我用一个简单的延迟发布者来模拟密码重置请求,当检测到无效的用户名时,它会失败(为了简单起见,只有包含 @ 的用户名才被认为是有效的)。发布者结果直接使用
.assign(to: &$state)
分配给发布的state
属性,这是一种非常方便的connect publishers方法:视图本身将视图模型示例化并存储为
@StateObject
。用户可以输入他们的名称并触发要求重置密码。每次视图模型状态更改时,都会自动触发body
更新,这允许视图进行适当的调整:上面的代码可以很容易地在Xcode项目中测试。