Only one alert may be attached to a view. To avoid collisions and clutter, one could handle alerts centrally using the func alert<Item>(item: Binding<Item?>...
modifier on a senior view.
The init above accepts a binding for any identifiable object and provides a closure to map it into multiple types of Alert
views. To minimize code in that closure and simplify call sites, below is a minimalist example wrapper and implementation. This isn’t complex at all, but while the API is decent it could be simpler.
@main
struct SomeApp: App {
@StateObject var coordinator: AppCoordinator
var body: some Scene {
WindowGroup() {
MainWindow()
.alert(item: coordinator.alert) { $0.present() }
}
}
class AppCoordinator: ObservableObject {
var alert: Binding<AlertItem?> {
Binding<AlertItem?> { self.root.state.alert }
set: { [weak self] value in self?.root.dispatch(.setAlert(value)) }
}
}
When an alert is dismissed, SwiftUI sets the provided binding’s value to nil. Your buttons don’t need to clean up alert presentation state.
public struct RootState {
var alert: AlertItem?
}
public struct AlertItem: Identifiable {
public var id = UUID()
public var title: Text
public var message: Text?
public var dismiss: Alert.Button?
public var primary: Alert.Button?
public var secondary: Alert.Button?
}
public extension AlertItem {
init(title: String,
message: String? = nil,
dismiss: Alert.Button? = nil) {
self.title = Text(title)
self.message = message == nil ? nil : Text(message!)
self.dismiss = dismiss
}
init(title: String,
message: String? = nil,
primary: Alert.Button? = nil,
secondary: Alert.Button? = nil) {
self.title = Text(title)
self.message = message == nil ? nil : Text(message!)
self.primary = primary
self.secondary = secondary
}
func present() -> Alert {
guard let primary = primary,
let secondary = secondary else {
return Alert(title: title,
message: message,
dismiss: dismiss)
}
return Alert(title: title,
message: message,
primary: primary,
secondary: secondary)
}
}
class FileMenuVM: VM {
func save() {
// If needs alert
root.dispatch(.setAlert(overwriteAlert))
}
lazy var overwriteAlert = AlertItem(title: Strings.Alerts.overwriteWarning,
message: nil,
primary: overwritePrimary,
secondary: overwriteSecondary)
lazy var overwritePrimary: Alert.Button = .default(Text(Strings.Alerts.overwriteWarningPrimary)) {
// Actions
}
lazy var overwriteSecondary: Alert.Button = .destructive(Text(Strings.Alerts.overwriteWarningDestructive)) {
// Actions
}
lazy var otherInitExample = AlertItem(title: Strings.Alerts.someAlert,
message: nil,
dismiss: nil)
}
Be aware that in macOS Big Sur in dark mode the destructive button on alerts is an illegible salmon-on-gray, with a WCAG contrast ratio of 1.7. Stick with AppKit. Feedback filed: FB9026454