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.@State
variable 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: