我是新手,正在学习Combine,并且在沟通方面遇到了一些问题。我的背景是Web开发和UIKit,与SwiftUI完全不同。
我非常希望使用MVVM将业务逻辑与视图层分离。这意味着除了可重用组件之外的任何视图都有一个ViewModel来处理API请求、逻辑、错误处理等。
我遇到的问题是,在ViewModel中发生某些事情时,将事件传递给View的最佳方法是什么?我知道View应该是状态的反映,但对于需要事件驱动的内容,它需要一堆变量,我认为这很混乱,因此很想尝试其他方法。
下面的示例是一个ForgotPasswordView,它作为一个sheet展示,当成功重置密码时,它应该关闭+显示一个成功的toast。如果失败,应该显示一个错误的toast(为上下文,全局的toast coordinator由根应用程序注入的@Environment变量管理)。
以下是一个有限的示例
View
上述的
在
以上是一个简单的例子,但如果需要进行更复杂的处理或数据操作,我不希望将其放在
对于几乎每个具有模型的视图,我都遇到了这个难题,如果
那是一大段文字,但我很想确保应用程序的架构是可维护的,易于测试,并且视图专注于显示数据和调用变异(但不能以在
谢谢
我非常希望使用MVVM将业务逻辑与视图层分离。这意味着除了可重用组件之外的任何视图都有一个ViewModel来处理API请求、逻辑、错误处理等。
我遇到的问题是,在ViewModel中发生某些事情时,将事件传递给View的最佳方法是什么?我知道View应该是状态的反映,但对于需要事件驱动的内容,它需要一堆变量,我认为这很混乱,因此很想尝试其他方法。
下面的示例是一个ForgotPasswordView,它作为一个sheet展示,当成功重置密码时,它应该关闭+显示一个成功的toast。如果失败,应该显示一个错误的toast(为上下文,全局的toast coordinator由根应用程序注入的@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
中,我可以通过onReceive
订阅它并处理响应。.onReceive(self.viewModel.$networkError, perform: { error in
if error {
// Call `closeSheet` and display toast
}
})
这种方法是可行的,但这只是一个示例,需要我为每个状态创建一个@Published
变量。而且,这些变量也必须进行清理(将它们设置回nil
)。
通过使用带有关联值的enum
可以使这个过程更加优雅,这样只需要使用单个监听器+变量。但是,枚举类型并不解决必须清理变量的问题。
使用PassthroughSubject
基于此,我研究了PassthroughSubject
,认为如果我创建一个单一的@Publisher
,如下:
@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
变量。
不幸的是,似乎不能使用 PassthroughSubject
来使用 onRecieve
。如果我将其设置为与第一个解决方案相同但为 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
中存在大量样板变量为代价)。谢谢
.onReceive
?你真正需要它的唯一事情似乎只是用于 dismiss。其他所有事情都可以在 ViewModel 中处理。ViewModel 在某种程度上类似于 UIKit 的UIViewController
,而View
在某种程度上类似于 storyboard。看起来你期望View
做的事情比它应该做的更多。 - lorem ipsumZStack
放在最外层View
中,您也可以在ViewModifer
中使用它。但是,当您将其放置在内部视图中时,问题就出现了。它将覆盖该内部视图。 - lorem ipsum