Setup:
I have a SwiftUI View that can present alerts. The alerts are provided by an AlertManager singleton by setting title and/or message of its published property @Published var nextAlertMessage = ErrorMessage(title: nil, message: nil). The View has a property @State private var presentingAlert = false.
This works when the following modifiers are applied to the View:
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
Problem:
Since alerts are also to be presented in other views, I wrote the following custom view modifier:
struct ShowAlert: ViewModifier {
@Binding var presentingAlert: Bool
let alertManager = AlertManager.shared
func body(content: Content) -> some View {
return content
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
and applied it to the View as:
.modifier(ShowAlert(presentingAlert: $presentingAlert))
However, no alerts are now shown.
Question:
What is wrong with my code and how to do it right?
Edit (as requested by Ashley Mills):
Here is a minimal reproducible example.
Please note:
In ContentView, the custom modifier ShowAlert has been out commented. This version of the code shows the alert.
If instead the modifiers .onAppear, .onChange and .alert are out commented, and the custom modifier is enabled, the alert is not shown.
// TestViewModifierApp
import SwiftUI
@main
struct TestViewModifierApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// ContentView
import SwiftUI
struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@State private var presentingAlert = false
var body: some View {
let alertManager = AlertManager.shared
let _ = alertManager.showNextAlertMessage(title: "Title", message: "Message")
Text("Hello, world!")
// .modifier(ShowAlert(presentingAlert: $presentingAlert))
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
// AlertManager
import SwiftUI
struct ErrorMessage: Equatable {
let title: String?
let message: String?
var joinedTitle: String {
(title ?? "") + "\n\n" + (message ?? "")
}
static func == (lhs: ErrorMessage, rhs: ErrorMessage) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message
}
}
final class AlertManager: NSObject, ObservableObject {
static let shared = AlertManager() // Instantiate the singleton
@Published var nextAlertMessage = ErrorMessage(title: nil, message: nil)
func showNextAlertMessage(title: String?, message: String?) {
DispatchQueue.main.async {
// Publishing is only allowed from the main thread
self.nextAlertMessage = ErrorMessage(title: title, message: message)
}
}
func alertConfirmed() {
showNextAlertMessage(title: nil, message: nil)
}
}
// ShowAlert
import SwiftUI
struct ShowAlert: ViewModifier {
@Binding var presentingAlert: Bool
let alertManager = AlertManager.shared
func body(content: Content) -> some View {
return content
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
You're over complicating this, the way to present an error alert is as follows:
LocalizedError. The simplest way to do it is anenum, with a case for each error your app can encounter. You have to implementvar errorDescription: String?, this is displayed as the alert title. If you want to display an alert message, then add a method to your enum to return this.@Statevariable to hold the error and one that's set when the alert should be presented. You can do it like this:but then you have two sources of truth, and you have to remember to set both each time. Alternatively, you can use a computed property for the
Bool:4. Extra Credit
As you did above, we can move a bunch of this stuff into a
ViewModifier, so we end up with:Now to display an error, all we need is: