使用 MVVM 在 SwiftUI 中显示警报

Posted

技术标签:

【中文标题】使用 MVVM 在 SwiftUI 中显示警报【英文标题】:Presenting an Alert in SwiftUI using MVVM 【发布时间】:2020-02-06 19:45:29 【问题描述】:

我正在尝试使用 SwiftUI 和 MVVM 架构构建应用程序。我想让我的 View 在它的 ViewModel 认为有必要时显示一个警报——比如,当它有一个来自 Model 的某种新结果时。因此,假设每当 VM 检测到新结果时,它都会相应地设置其 status

ViewModel:

enum Status 
    case idle
    case computing
    case newResultAvailable


class MyViewModel: ObservableObject 

    @Published var status = Status.idle

    ...

观点:

struct ContentView: View 

    @ObservedObject var vm = MyViewModel()

    @State private var announcingResult = false 
        didSet 
            // reset VM status when alert is dismissed
            if announcingResult == false 
                vm.status = .idle
            
        
    

    var body: some View 
        Text("Hello")
        .alert(isPresented: $announcingResult) 
            Alert(title: Text("There's a new result!"),
                message: nil,
                dismissButton: .default(Text("OK")))
        
    

Apple 设计了 ​​.alert() 修饰符以将绑定作为其第一个参数,以便在绑定属性变为 true 时显示警报。然后,当警报解除时,绑定属性会自动设置为false

我的问题是: 每当 VM 的 status 变为 .newResultAvailable 时,如何让警报出现?在我看来,这就是正确的 MVVM 应该如何发挥作用,并且感觉非常符合所有 SwiftUI WWDC 演示的精神,但我找不到方法……

【问题讨论】:

是否有任何文档参考或解释说明为什么这不起作用?我似乎无法理解这背后的原因。 @Rikh 只要将announcingResult 设置为true,它就可以工作。但我的目标是在vm.status 变为某个值时发出警报……而上面的代码没有任何规定可以做到这一点。这就是为什么@asperi 在下面的回答是必要的。 啊,我看错了你上面的例子。道歉。我发现在视图模型中直接使用@Published 值来控制警报也不会导致它出现在View 中。所以一直在寻找答案! 【参考方案1】:

这是可能的方法(经过测试并适用于 Xcode 11.3+)

struct ContentView: View 

    @ObservedObject var vm = MyViewModel()

    var body: some View 
        let announcingResult = Binding<Bool>(
            get:  self.vm.status == .newResultAvailable ,
            set:  _ in self.vm.status = .idle 
        )
        return Text("Hello")
            .alert(isPresented: announcingResult) 
                Alert(title: Text("There's a new result!"),
                    message: nil,
                    dismissButton: .default(Text("OK")))
            
    

有时以下符号可能更可取

var body: some View 
    Text("Hello")
        .alert(isPresented: Binding<Bool>(
            get:  self.vm.status == .newResultAvailable ,
            set:  _ in self.vm.status = .idle 
        )) 
            Alert(title: Text("There's a new result!"),
                message: nil,
                dismissButton: .default(Text("OK")))
        

【讨论】:

谢谢你,@Asperi!这看起来棒极了。我会试一试。我没有想到我可能必须进行手动绑定……到目前为止我从来没有这样做过,但这似乎是一个优雅的解决方案。如果有一个可以避免它们,我会很高兴;如果没有,我认为这会起作用! 正如@user12208004 指出的那样,我对绑定的覆盖在 Xcode 11.5 中不再起作用。这种行为似乎被打破了。 我没有 Xcode 11.5。使用 Xcode 12 重新测试 - 工作正常。确保在主队列上修改视图模型发布的属性。 我更新到 Xcode 11.6,它也可以正常工作。谢谢@Asperi【参考方案2】:

我创建了一个辅助类

class AlertProvider 
    struct Alert 
        var title: String
        let message: String
        let primaryButtomText: String
        let primaryButtonAction: () -> Void
        let secondaryButtonText: String
    

    @Published var shouldShowAlert = false

    var alert: Alert? = nil  didSet  shouldShowAlert = alert != nil  

在VM中可以这样使用

var alertProvider = AlertProvider()

func showAlert() 
    alertProvider.alert = AlertProvider.Alert(
        title: "The Locatoin Services are disabled",
        message: "Do you want to turn location on?",
        primaryButtomText: "Go To Settings",
        primaryButtonAction:  [weak self] in
            self?.appSettingsHandler.openAppSettings()
        ,
        secondaryButtonText: "Cancel"
    )

我们还可以使用警报视图类的扩展

extension Alert 
    init(_ alert: AlertProvider.Alert) 
        self.init(title: Text(alert.title),
                  message: Text(alert.message),
                  primaryButton: .default(Text(alert.primaryButtomText),
                                          action: alert.primaryButtonAction),
                  secondaryButton: .cancel(Text(alert.secondaryButtonText)))
    

那么View会以如下方式使用它

.alert(isPresented: $viewModel.alertProvider.shouldShowAlert ) 
        guard let alert = viewModel.alertProvider.alert else  fatalError("Alert not available") 

        return Alert(alert)

我相信这种方法可以进一步改进

【讨论】:

您好,感谢您的回答。您是否不需要将来自 AlertProvider 类 (shouldShowAlert) 的更改从 VM 内部发送到您的视图,然后才能正常工作,还是我遗漏了什么?

以上是关于使用 MVVM 在 SwiftUI 中显示警报的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有按钮的 SwiftUI 中显示警报

SwiftUI 基于计算属性显示警报

SwiftUI 中的全局警报

SwiftUI MapKit UIViewRepresentable 无法显示警报

如何在 SwiftUI 中一个接一个地显示多个警报对话框?

当用户输入的时间低于预期时间时,如何在 swiftUI 中显示警报消息?