swiftswiftuialert

Managing several Alerts for the same Button while avoiding the deprecated Alert struct and using the instance method instead


Before Alert became deprecated, I used to manage different Alerts of a View by introducing an AlertState:

enum AlertState: Identifiable {
    case associatedIngredient
    case confirmDelete
    
    var id: AlertState { self }
}

I had variable in my View:

@State private var activeAlert: FoodItemViewSheets.AlertState?

which I would set the AlertState with:

// Delete the food item
Button("Delete", systemImage: "trash") {
    if foodItemVM.hasAssociatedFoodItem() {
        // Check if FoodItem is related to an Ingredient
        if !foodItemVM.canBeDeleted() {
            self.activeAlert = .associatedIngredient
        } else {
            self.activeAlert = .confirmDelete
        }
    }
}

... and later:

.alert(item: $activeAlert) {
    alertContent($0)
}

This function would then deliver the right Alert:

private func alertContent(_ state: AlertState) -> Alert {
    switch state {
    case .confirmDelete:
        return Alert(
            title: Text("Delete food"),
            message: Text("Do you really want to delete this food item? This cannot be undone!"),
            primaryButton: .default(
                Text("Do not delete")
            ),
            secondaryButton: .destructive(
                Text("Delete"),
                action: deleteFoodItemOnly
            )
        )
    case .associatedIngredient:
        return Alert(
            title: Text("Cannot delete food"),
            message: Text("This food item is in use in a recipe, please remove it from the recipe before deleting.")
        )
    }
}

As Alert is deprecated, how to get this kind of behavior (two different alerts associated with the same button) to work with the new alert(_:isPresented:presenting:actions:message:) instance method?


Solution

  • You only need one extra @State for passing to the isPresented: parameter. The rest are all pretty self-explanatory. You'd just use switch statements to provide the title, message, and actions as you were already doing. It's just that you are returning View, instead of Alert.

    @State private var alertPresented = false
    @State private var activeAlert: AlertState?
    
    // this flag controls which alert to show in this toy example
    @State private var flag = false
    
    var body: some View {
        // as an example, I will use a toggle to control which alert to show
        Toggle("", isOn: $flag)
        Button("Delete", systemImage: "trash") {
            if flag {
                self.activeAlert = .associatedIngredient
            } else {
                self.activeAlert = .confirmDelete
            }
        }
        .alert(alertTitle, isPresented: $alertPresented, presenting: activeAlert) {
            alertActions(for: $0)
        } message: {
            alertMessage(for: $0)
        }
        .onChange(of: activeAlert) { oldValue, newValue in
            if oldValue == nil && newValue != nil {
                alertPresented = true
            } else if oldValue != nil && newValue == nil {
                alertPresented = false
            }
        }
    
    }
    
    @ViewBuilder
    func alertMessage(for alert: AlertState) -> some View {
        switch alert {
        case .associatedIngredient:
            Text("Do you really want to delete this food item? This cannot be undone!")
        case .confirmDelete:
            Text("This food item is in use in a recipe, please remove it from the recipe before deleting.")
        }
    }
    
    @ViewBuilder
    func alertActions(for alert: AlertState) -> some View {
        switch alert {
        case .associatedIngredient:
            Button("OK", role: .cancel) {}
        case .confirmDelete:
            Button("Do not delete", role: .cancel) { }
            Button("Delete", role: .destructive) {
                // ...
            }
        }
    }
    
    var alertTitle: String {
        switch activeAlert {
        case .associatedIngredient:
            "Cannot delete food"
        case .confirmDelete:
            "Delete food"
        case nil:
            ""
        }
    }