如何在SwiftUI中一个视图上有两个警报?

49

我希望在同一个Button视图上附加两个不同的警报。当我使用下面的代码时,只有底部的警报能够正常工作。

我正在使用macOS Catalina上的Xcode 11官方版本。

@State private var showFirstAlert = false
@State private var showSecondAlert = false

Button(action: {
    if Bool.random() {
        showFirstAlert = true
    } else {
        showSecondAlert = true
    }
}) {
    Text("Show random alert")
}
.alert(isPresented: $showFirstAlert) {
    // This alert never shows
    Alert(title: Text("First Alert"), message: Text("This is the first alert"))
}
.alert(isPresented: $showSecondAlert) {
    // This alert does show
    Alert(title: Text("Second Alert"), message: Text("This is the second alert"))
}

当我将showFirstAlert设置为true时,我希望第一个警报显示出来,并且当我将showSecondAlert设置为true时,我希望第二个警报显示。只有第二个警报的状态为true时才会显示,但第一个警报则什么都不做。


你从未将 showFirstAlertshowSecondAlert 设置为 false - LinusGeffarth
2
SwiftUI会在用户关闭警告时自动将它们设置为“false”。 - Luke Chambers
1
啊,我的错。谢谢提醒! - LinusGeffarth
1
当 SwiftUI 视图中的一个来自外部包并使用 .alert 显示消息时,这种情况特别隐蔽。(这让我想起了 UIKit 中 UIAlertView 的问题 - 尝试在视图控制器层次结构的多个级别中使用它们)。 - Chris Prince
10个回答

80

第二次调用.alert(isPresented)正在覆盖第一次的调用。你真正希望的是一个Binding<Bool>来表示警报是否已呈现,以及在.alert(isPresented)后的闭包中应返回哪个警报。您可以使用Bool来实现这一点,但我已经使用枚举完成了此操作,因为它可扩展到两个以上的警报。

enum ActiveAlert {
    case first, second
}

struct ToggleView: View {
    @State private var showAlert = false
    @State private var activeAlert: ActiveAlert = .first

    var body: some View {

        Button(action: {
            if Bool.random() {
                self.activeAlert = .first
            } else {
                self.activeAlert = .second
            }
            self.showAlert = true
        }) {
            Text("Show random alert")
        }
        .alert(isPresented: $showAlert) {
            switch activeAlert {
            case .first:
                return Alert(title: Text("First Alert"), message: Text("This is the first alert"))
            case .second:
                return Alert(title: Text("Second Alert"), message: Text("This is the second alert"))
            }
        }
    }
}

这个设置似乎很好,但是在像这样的警报设置中放置在主按钮中的任何操作都不会触发。有什么想法吗? - Michael
@Michael,我没有任何问题设置主按钮操作或次要操作。可能是(1)您的代码存在单独的错误,或者(2)您需要同时指定主要和次要操作,尽管这只是一种猜测。 - nonamorando

31

这种解决方案有一个变体,只使用一个状态变量而不是两个。 它利用了另一个采用 Identifiable 而不是 Bool 的 .alert() 表单的事实,因此可以传递额外的信息:

struct AlertIdentifier: Identifiable {
    enum Choice {
        case first, second
    }

    var id: Choice
}

struct ContentView: View {
    @State private var alertIdentifier: AlertIdentifier?

    var body: some View {
        HStack {
            Button("Show First Alert") {
                self.alertIdentifier = AlertIdentifier(id: .first)
            }
            Button("Show Second Alert") {
                self.alertIdentifier = AlertIdentifier(id: .second)
            }
        }
        .alert(item: $alertIdentifier) { alert in
            switch alert.id {
            case .first:
                return Alert(title: Text("First Alert"),
                             message: Text("This is the first alert"))
            case .second:
                return Alert(title: Text("Second Alert"),
                             message: Text("This is the second alert"))
            }
        }
    }
}

var id: Choice could be let id: Choice - Borzh
1
这个现在已经被弃用了。 - Anshul

7

如果您有更复杂的逻辑(例如,从一个按钮获得多个警报),这里有另一种灵活的方法。您可以将.alert附加到任何View中,并像这样将警报逻辑与按钮分开:

EmptyView()对我不起作用。在Xcode 12.4中进行了测试。

// loading alert
Text("")
    .alert(isPresented: $showLoadingAlert, content: {
        Alert(title: Text("Logging in"))
    })
    .hidden()

// error alert
Text("")
    .alert(isPresented: $showErrorAlert, content: {
        Alert(title: Text("Wrong passcode"), message: Text("Enter again"), dismissButton: .default(Text("Confirm")))
    })
    .hidden()

2
将frame(width:0, height:0)添加到视图中以隐藏它。如果我隐藏了视图,代码就无法工作。 - jonye._.jin
1
这个可行!一个改进的方法是创建一个可重用的BlankView。 https://gist.github.com/4brunu/d75d9bd7f62d95f0b4830d3c56eb3c61 - Bruno Coelho

6

我对Ben的回答进行了一些改进。 您可以使用.alert(item:)而不是.alert(isPresented:)动态显示多个警报:

struct AlertItem: Identifiable {
    var id = UUID()
    var title: Text
    var message: Text?
    var dismissButton: Alert.Button?
}

struct ContentView: View {

    @State private var alertItem: AlertItem?

    var body: some View {
        VStack {
            Button("First Alert") {
                self.alertItem = AlertItem(title: Text("First Alert"), message: Text("Message"))
            }
            Button("Second Alert") {
                self.alertItem = AlertItem(title: Text("Second Alert"), message: nil, dismissButton: .cancel(Text("Some Cancel")))
            }
            Button("Third Alert") {
                self.alertItem = AlertItem(title: Text("Third Alert"))
            }
        }
        .alert(item: $alertItem) { alertItem in
            Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
        }
    }
}

如果标题或消息是动态的,该如何处理? - Sree
1
已弃用。请改用 `alert(title:isPresented:presenting::actions:)。 - Anshul

5
注意,在iOS 16中,一个视图上有两个警告不再是问题。 在本线程中提到的alert(isPresented:content:)alert(item:content:)以及Alert结构体已经弃用。
建议直接使用alert(_:isPresented:actions:message:)或其变化之一。例如:
struct ContentView: View {
    @State private var isFirstAlertPresented = false
    @State private var isSecondAlertPresented = false

    var body: some View {
        VStack {
            Button("Show first alert") {
                isFirstAlertPresented = true
            }
            Button("Show second alert") {
                isSecondAlertPresented = true
            }
        }
        .alert(
            "First alert",
            isPresented: $isFirstAlertPresented,
            actions: {
                Button("First OK") {}
            },
            message: {
                Text("First message")
            }
        )
        .alert(
            "Second alert",
            isPresented: $isSecondAlertPresented,
            actions: {
                Button("Second OK") {}
            },
            message: {
                Text("Second message")
            }
        )
    }
}

也适用于iOS 15.6.1。 - Dale
恭喜啊,我的朋友,终于他们为这个 bug 做了些什么…… - Alessandro Pace

2

我想分享一种处理多个警报的酷炫策略。我从Hacking with Swift(参见此处@bryceac的帖子:https://www.hackingwithswift.com/forums/swiftui/exporting-multiple-file-types/13298)中得到了灵感,该帖子讨论了在视图模型中更改文件导出器的文档和文档类型。对于警报,您可以做同样的事情(至少在许多情况下)。最简单的警报只有一个信息性的标题和消息。如果您有一堆需要显示的警报,您可以在视图模型中更改字符串。例如,您实际的警报代码可能如下所示:

.alert(isPresented: $viewModel.showingAlert) {
    Alert(title: Text(viewModel.alertTitle), message: Text(viewModel.alertMessage), dismissButton: .default(Text("Got it.")))
}

虽然这可能导致一些有趣的字符串传递,如果您需要在主视图模型之外更新它们,但我发现它运行良好(我尝试像Paul Hudson在Hacking with Swift中建议的那样将不同的警报添加到不同的视图中却遇到了问题),而且我喜欢在需要通知用户时不必说出10个警报的情况下,可以处理许多不同结果。

但我认为使用枚举更好,正如John M(https://stackoverflow.com/users/3088606/john-m)建议的。例如:

enum AwesomeAlertType {
    case descriptiveName1
    case descriptiveName2
}

对于简单的警告,您可以使用一个函数来构建它们,该函数使用标题和消息以及一个按钮标题,其默认值由您选择:

func alert(title: String, message: String, buttonTitle: String = "Got it") -> Alert {
    Alert(title: Text(title), message: Text(message), dismissButton: .default(Text(buttonTitle)))
}

然后,您可以像下面这样做:

然后,您可以执行以下操作:

.alert(isPresented: $viewModel.showingAlert) {
    switch viewModel.alertType {
    case descriptiveName1:
        return alert(title; "My Title 1", message: "My message 1")
    case descriptiveName2:
        return alert(title; "My Title 2", message: "My message 2")
    default:
        return alert(title: "", message: "")
    }
}

这让你可以一次性声明警报界面,使用枚举和一个可在视图模型中赋值的bool绑定来控制其状态,并通过使用函数生成带有标题、消息和按钮标题的基本警报(有时候这就够了),从而使代码简短干净。请保留所有html标签。

2
extension Alert:Identifiable{
    public var id:String { "\(self)" }
}

@State var alert:Alert?

Button(action: {
    if Bool.random() {
        alert = Alert(title: Text("Alert 1"))
    } else {
        alert = Alert(title: Text("Alert 2"))
    }
}) {
    Text("Show random alert")
}
.alert(item:$alert) { $0 }

0

这个问题有两种解决方案。一种是将你的.alert附加到另一个视图上,例如生成警报的按钮。这是最好的解决方案,但根据视图的不同并不总是适用。另一种选择是以下方法,可以显示任何警报,与接受的答案相比。

@State var isAlertShown = false
@State var alert: Alert? {
    didSet {
        isAlertShown = alert != nil
    }
}

YourViews {
    Button(action: {
        alert = Alert(title: Text("BACKUP"), message: Text("OVERWRITE_BACKUP_CONFIRMATION"), primaryButton: .destructive(Text("OVERWRITE")) {
            try? BackupManager.shared.performBackup()
        }, secondaryButton: .cancel())
    }, label: {
        Text("Button")
    })
}
.alert(isPresented: $isAlertShown, content: {
    guard let alert = alert else { return Alert(title: Text("")) }
        
    return alert
})

0

和其他人发布的类似,这是我的方法。这提供了一些便利,但也允许自定义警报。

/// A wrapper item for alerts so they can be identifiable
struct AlertItem: Identifiable {
    let id: UUID
    let alert: Alert
    
    /// Initialize this item with a custom alert
    init(id: UUID = UUID(), alert: Alert) {
        self.id = id
        self.alert = alert
    }
    
    /// Initialize this item with an error
    init(id: UUID = UUID(), title: String = "Oops", error: Error) {
        self.init(id: id, title: title, message: error.localizedDescription)
    }
    
    /// Initialize this item with a title and a message
    init(id: UUID = UUID(), title: String, message: String? = nil) {
        let messageText = message != nil ? Text(message!) : nil
        
        self.id = id
        self.alert = Alert(
            title: Text(title),
            message: messageText,
            dismissButton: .cancel()
        )
    }
    
    /// Convenience method for displaying simple messages
    static func message(_ title: String, message: String? = nil) -> Self {
        return Self.init(title: title, message: message)
    }
    
    /// Convenience method for displaying localizable errors
    static func error(_ error: Error, title: String = "Oops") -> Self {
        return Self.init(title: title, error: error)
    }
    
    /// Convenience method for displaying a custom alert
    static func alert(_ alert: Alert) -> Self {
        return Self.init(alert: alert)
    }
}

extension View {
    func alert(item: Binding<AlertItem?>) -> some View {
        return self.alert(item: item) { item in
            return item.alert
        }
    }
}

现在你可以像这样使用你的alertItem:

struct ContentView: View {
    @Binding private let alertItem: AlertItem?
    
    var body: some View {
        VStack {
            Button("Click me", action: {
                alertItem = .message("Alert title", message: "Alert message")
            })
            
            Button("Click me too", action: {
                alertItem = .message("Alert title 2", message: "Alert message 2")
            })
        }.alert(item: $alertItem)
    }
}

0

.alert(isPresented: $invalidPhotosOrServerError) { Alert(title: Text(invalidPhotos ? "上传的照片无效" : "调用服务器时出错"), message: Text(invalidPhotos ? "请上传有效的照片" : "调用服务器时出现问题"), dismissButton: .cancel()) }

当上传无效照片时,将invalidPhotosOrServerErrorinvalidPhotos都赋值为true。当出现服务器错误时,只将invalidPhotosOrServerError赋值为true


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接