SwiftUI silently ignored save error alerts once a second alert existed, and this post captures what we learned in the Advanced SwiftUI: Lessons From Mistakes series.

This is Part 2 in our Advanced SwiftUI: Lessons From Mistakes series:

  • Part 1 – ObservedOrStateObject
    walks through the property-wrapper bug that kicked off this learning arc.

SwiftUI’s .alert modifier looks deceptively simple until you attach more than one alert to the same view hierarchy. This post documents a regression that prevented save failures from surfacing to users in both the Edit Product and Add Product flows, how we diagnosed the issue, and the architectural changes that fixed it.

Context

We noticed that when a save failed in Edit Product, the expected alert never appeared, even though logs showed the failure and showingSaveError was set to true.

Inspecting EditProductSheet revealed two separate .alert modifiers:

.alert(Strings.Errors.saveFailed, isPresented: $showingSaveError) { ... }
// ... later in the view tree
.alert(Strings.Printer.printJobFailed, isPresented: ...) { ... }

SwiftUI allows only one alert per view at a time. Once the printer alert had been registered, subsequent alerts were ignored even if their bindings flipped to true. The framework never complained; it just silently failed to present the alert. The same pattern existed in AddProductSheet, so users there were also missing save errors.

Root Cause

  • Multiple .alert modifiers attached to the same view hierarchy.
  • SwiftUI silently discards secondary alerts when one is already active.
  • Save failures toggled state, but UI never presented the overlay.

Solution Overview

We introduced a shared ProductSheetAlertType enum and consolidated alert presentation through a single .alert(item:) modifier. Both sheets now set an activeAlert state that determines which message appears, ensuring only one alert configuration is registered per view.

Shared Alert Type

enum ProductSheetAlertType: Identifiable {
    case saveError(String)
    case printerError(String)

    var id: String {
        switch self {
        case .saveError: return "saveError"
        case .printerError: return "printerError"
        }
    }
}

Edit Product Sheet Update

@State var activeAlert: ProductSheetAlertType?
// ...
.alert(item: $activeAlert) { alertType in
    switch alertType {
    case .saveError(let message):
        Alert(
            title: Text(Strings.Errors.saveFailed),
            message: Text(message),
            dismissButton: .cancel(Text(Strings.Common.confirmation))
        )
    case .printerError(let message):
        Alert(
            title: Text(Strings.Printer.printJobFailed),
            message: Text(message),
            dismissButton: .cancel(Text(Strings.Common.confirmation))
        )
    }
}
.onChange(of: viewModel.saveError) { _, newError in
    if let error = newError { activeAlert = .saveError(error) }
}
.onChange(of: viewModel.printError) { _, newError in
    if let error = newError { activeAlert = .printerError(error) }
}

Add Product Sheet Update

AddProductSheet mirrors the same approach: it keeps its own activeAlert state, wires viewModel.saveError and viewModel.printError into that binding, and routes all messaging through the shared .alert(item:). The copy-paste-prone double-alert setup is gone, so future alerts plug into the enum instead of stacking additional .alert modifiers.

Testing

Focused suites covering EditProductSheet, EditProductSheetActions, EditProductSheetView, and AddProductSheet all passed, and manual QA confirmed that the alert now appears when save or printer discovery failures occur.

Takeaways

  1. Single Alert Rule: SwiftUI only respects one alert per view hierarchy. Consolidate via .alert(item:) when multiple surfaces might need to display messages.
  2. Shared Abstractions Help: Centralizing ProductSheetAlertType prevents drift between sheets and keeps future alert additions straightforward.
  3. Test Coverage Matters: Focused test plans caught regressions across Add and Edit flows, preserving confidence in the refactor.

FAQ

Why did our SwiftUI save error alerts stop showing when multiple alerts were attached to a view?
SwiftUI only respects one alert configuration per view hierarchy. Once a second .alert modifier was registered, the framework silently ignored subsequent alerts, so the save error binding flipped to true but no overlay appeared.
How does using a shared alert enum and a single alert(item:) call fix the issue?
A shared ProductSheetAlertType enum and a single alert(item:) modifier ensure only one alert definition exists. Different error states just set activeAlert to different enum cases, and SwiftUI presents the appropriate message without conflicting alert configurations.

Welcome to The infinite monkey theorem

Somewhere a monkey just typed Shakespeare in TypeScript. Be the first to read the masterpieces (and the hilarious misfires) landing on the blog.

Subscribe to The infinite monkey theorem

We fling fresh posts—no banana peels attached—straight to your inbox.