Luke Chambers
Luke Chambers

Reputation: 905

How can I have two alerts on one view in SwiftUI?

I want to have two unique alerts attached to the same Button view. When I use the code below, only the alert on the bottom works.

I'm using the official release of Xcode 11 on macOS Catalina.

@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"))
}

I expect first alert to show when I set showFirstAlert to true and I expect the second alert to show when I set showSecondAlert to true. Only the second alert shows when its state is true but the first one does nothing.

Upvotes: 59

Views: 15860

Answers (11)

user3412355
user3412355

Reputation: 53

I had to do this ugly thing:

    private func alerts(view: any View) -> some View {
    let result = view.overlay {
        ZStack {
            ZStack { }
                .alert(viewModel.deleteButtonText, isPresented: $showingConfirmDeleteItems) {
                    Button("Cancel", role: .cancel) {}
                    Button("Delete", role: .destructive) {
                        onDeleteItemsConfirmedClicked()
                    }
                } message: {
                    Text(String(format: viewModel.deleteConfirmText, selection.count > 0 ? " (\(selection.count))" : ""))
                }
            ZStack { }
                .alert(isPresented: $showCameraSettingsAlert, content: {
                    SettingsAlert.alert(title: "Camera Access", message: viewModel.cameraSettingsAlertText, primaryButtonText: "Open Settings", isPresented: $showCameraSettingsAlert)
                })
            ZStack { }
                .alert(isPresented: $showMicrophoneSettingsAlert, content: {
                    SettingsAlert.alert(title: "Microphone Access", message: viewModel.microphoneSettingsAlertText, primaryButtonText: "Open Settings", isPresented: $showMicrophoneSettingsAlert)
                })
            ZStack { }
                .alert(isPresented: $showPhotosVideosSettingsAlert, content: {
                    SettingsAlert.alert(title: "Photos Access", message: viewModel.photosVideosSettingsAlertText, primaryButtonText: "Open Settings", isPresented: $showPhotosVideosSettingsAlert)
                })
        }
    }
    return AnyView(result)
}

Upvotes: 0

Nouru Muneza
Nouru Muneza

Reputation: 77

.alert(isPresented: $invalidPhotosOrServerError) { Alert(title: Text(invalidPhotos ? "Invalid photos Uploaded" : "Error calling server"), message: Text(invalidPhotos ? "PLease upload valid photos": "Something went wrong calling server" ), dismissButton: .cancel()) }

Then when invalid photos are uploaded assign both invalidPhotosOrServerError and invalidPhotos. When there is a server error only assign invalidPhotosOrServerError to true.

Upvotes: 0

marcinax
marcinax

Reputation: 1195

Note that in iOS 16 having two alerts on one view is not an issue anymore. alert(isPresented:content:) and alert(item:content:) mentioned in many of the answers in this thread are deprecated as well as the Alert struct.

It's recommended to simply use alert(_:isPresented:actions:message:) instead or one of its variations. For instance:

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")
            }
        )
    }
}

Upvotes: 10

mattroberts
mattroberts

Reputation: 630

I want to share a cool strategy for handling multiple alerts. I got the idea from someone's post on Hacking with Swift (see here @bryceac's post: https://www.hackingwithswift.com/forums/swiftui/exporting-multiple-file-types/13298) about changing the document and document types for a file exporter in a view model. You can do the same things (in a lot of cases at least) for alerts. The simplest alerts just have an informative title and message. If you have a bunch of alerts you potentially need to display, you can change the Strings in a view model. For example, your actual alert code may look like this

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

Though this can result in some funny passing around of Strings if you need to update them in other places then your main view model, I find it works well (and I had trouble just adding different alerts to different views as Paul Hudson suggested on Hacking with Swift for whatever reason) and I like not having say, 10 alerts in the event that a lot of different results can occur of which you need to notify the user.

But I think using an enum is better, as John M (https://stackoverflow.com/users/3088606/john-m) suggested. For example:

enum AwesomeAlertType {
    case descriptiveName1
    case descriptiveName2
}

For simple alerts, you can have a function that builds them using a title and a message, and a button title with a default value of your choosing:

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

Then, you can use do something like the following:

.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: "")
    }
}

This lets you declare your alert UI in one block, control its state with an enum and a binding to a bool that can be assigned in view models, and keep your code short and DRY by using a function to produce basic alerts (sometimes all you need) with a title and a message, and a button with a title.

Upvotes: 2

Jacob
Jacob

Reputation: 1062

Similar to what other people posted, here is my approach. This provides some conveniences but allows for custom alerts.

/// 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
        }
    }
}

now you can just use your alertItem like this:

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)
    }
}

Upvotes: 1

Maciej Swic
Maciej Swic

Reputation: 11359

There are two solutions to this. Either attach your .alert to another view, for example the button generating the alert. This is the best solution, but doesn't always work depending on the view. The other option is the following, which can display any alert compared to the accepted answer.

@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
})

Upvotes: 0

jonijeng
jonijeng

Reputation: 305

Here's another flexible way to do it if you have more complex logics (eg. multiple alerts from 1 button). You can basically attach .alert to any View and separate the alert logic from buttons like this:

EmptyView() did not work for me. Tested in 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()

Upvotes: 7

Sky Liu
Sky Liu

Reputation: 21

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 }

Upvotes: 2

aslebedev
aslebedev

Reputation: 443

I improved a litle Ben's answer. You can show multiple alerts dynamically by using .alert(item:) instead .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)
        }
    }
}

Upvotes: 7

Ben
Ben

Reputation: 2081

There's a variation on this solution which only uses one state variable rather than two. It uses the fact that there is another .alert() form which takes an Identifiable item rather than a Bool, so extra information can be passed in that:

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"))
            }
        }
    }
}

Upvotes: 33

John M.
John M.

Reputation: 9463

The second call to .alert(isPresented) is overriding the first. What you really want is one Binding<Bool> to denote whether the alert is presented, and some setting for which alert should be returned from the closure following .alert(isPresented). You could use a Bool for this, but I went ahead and did it with an enum, as that scales to more than two alerts.

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"))
            }
        }
    }
}

Upvotes: 93

Related Questions