SwiftUI通用Toast视图修饰符不响应ViewModel状态变更问题
问题分析与解决方案
问题描述
我用Toast库做错误/成功提示,为了避免重复写代码,封装了通用视图修饰符ErrorToast,但遇到了问题:从页面A跳转到页面B执行操作触发错误时,Toast完全没反应;返回A再重新进入B页面,Toast才会显示。这说明error变量确实被正确赋值了,但视图没响应变量的更新。
相关代码
1. ErrorToast 视图修饰符
struct ErrorToast: ViewModifier { let actionCompletedViewModel: ActionCompletedViewModel func body(content: Content) -> some View { content .toast(isPresenting: .constant(actionCompletedViewModel.error != nil), duration: 2, tapToDismiss: true, alert: { let errorMessage = actionCompletedViewModel.error?.errorDescription let errorTitle = actionCompletedViewModel.error?.title return AlertToast(displayMode: .alert, type: .error(.red), title: errorTitle, subTitle: errorMessage) }, onTap: { actionCompletedViewModel.handleErrorAndClear() }) } }
2. 父类 ViewModel
class ActionCompletedViewModel: ObservableObject { @Published var error: CustomError? @Published var success: CustomSuccess? func assignError(error: CustomError) { DispatchQueue.main.async {[weak self] in self?.error = error } } // 其他代码省略 }
3. 业务视图与子类 ViewModel
struct AView: View { @ObservedObject var aViewModel = AViewModel.shared var body: some View { VStack { Button { aViewModel.doAction() } label: { Text("执行操作") } } .errorToast(aViewModel: aViewModel) } } class AViewModel: ActionCompletedViewModel, ObservableObject { func doAction() { someOtherAction() { result in DispatchQueue.main.async { switch result { case .success(let success): self.assignSuccess(success: success) case .failure(let error): self.assignError(error: error) } } } } // 异步操作模拟代码省略 }
问题根源
- 静态绑定导致不响应更新:
isPresenting用了.constant(actionCompletedViewModel.error != nil),这是个一次性计算的静态值,只有视图第一次渲染时会判断,后续error变化完全不会触发绑定更新,自然弹不出Toast。 - ObservableObject监听方式错误:之前尝试给
ErrorToast里的actionCompletedViewModel加@ObservedObject但没用,因为原来的属性是let声明的,@ObservedObject只能修饰var类型的属性,才能监听ViewModel的@Published变量变化。
修复方案
修改ErrorToast的实现,改成能动态监听error变化的形式:
struct ErrorToast: ViewModifier { @ObservedObject var actionCompletedViewModel: ActionCompletedViewModel func body(content: Content) -> some View { content .toast(isPresenting: $actionCompletedViewModel.error.isNotNilBinding, duration: 2, tapToDismiss: true, alert: { AlertToast( displayMode: .alert, type: .error(.red), title: actionCompletedViewModel.error?.title, subTitle: actionCompletedViewModel.error?.errorDescription ) }, onTap: { actionCompletedViewModel.handleErrorAndClear() }) } } // 给Optional扩展双向绑定的判断属性 extension Optional { var isNotNil: Bool { self != nil } var isNotNilBinding: Binding<Bool> { Binding( get: { self.isNotNil }, set: { newValue in if !newValue { self = nil } } ) } } // 给View加便捷调用方法 extension View { func errorToast(viewModel: ActionCompletedViewModel) -> some View { self.modifier(ErrorToast(actionCompletedViewModel: viewModel)) } }
关键修改点
- 把
ErrorToast里的let actionCompletedViewModel改成@ObservedObject var actionCompletedViewModel,这样就能监听到ViewModel中@Published变量的变化。 - 用自定义的
isNotNilBinding替代.constant,实现双向响应:error赋值时自动弹出Toast,Toast关闭时自动清空error,避免重复触发。 - 补充
handleErrorAndClear的实现(如果之前没写):
class ActionCompletedViewModel: ObservableObject { // 原有代码... func handleErrorAndClear() { DispatchQueue.main.async {[weak self] in self?.error = nil } } }
这样修改后,当error被赋值时,ErrorToast会立刻感知到变化并弹出Toast;关闭Toast时也会自动清空error,确保下次错误能正常触发提示。
内容的提问来源于stack exchange,提问作者ALex
相关产品推荐
相关产品推荐

