Reputation: 905
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
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
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
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
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
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
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
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
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
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
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
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