Correctly modelling error alerts/banners (there can be only one)

I'm currently trying to think about how to best model the display of errors within our app and I'm struggling to come up with the right approach.

There is some prior art and ideas already available, such as the built-in alert/action sheet functionality in TCA and also some previous discussion here:

The simple approach would be, for a given module (e.g. FeatureAState, FeatureAAction), to have that feature's state have some kind of banner or alert property but there seems to be some limitations around modelling things this way:

  • You need to repeat this everywhere - duplicating properties and actions in each model that needs to display some kind of error alert.
  • You need to repeat the UI code so each view reacts to this duplicate state and displays the appropriate UI.
  • It potentially allows for an invalid state across your application.

The last point in particular is what makes me feel like modelling state in this way is not the right approach. Correctly modelled state should not allow you to create an invalid combination of states. Its why we might use enum to represent exclusive states rather than a mix of properties on a struct for 'example. So how does it apply here? Conceptually, for most apps, you can only be displaying a single banner or error alert at a time, across your whole app. On iOS, this is especially true for alerts.

So how is your UI supposed to react if you have struct FeatureA and struct FeatureB both with an alert property and both set to some value? There is no way to enforce the idea that only one alert can be set across your entire state tree at a time modelled this way.

The obvious way to model this would be for there to be a single state property in your root AppState however you're now faced with the issue of how you bubble error states up from your child reducers. They cannot directly set the app state, nor can they return an effect that sends some root action.

Is the only way to approach this to have a combination of both? Have a single root alert state that you then share across your sub-states so that sub-states just mutate their own local state but this state mutation is just a proxy for mutating the root state, as in the "Shared State" case study?

This would seem to solve the "invalid state" problem and would also seem to solve the duplicate UI code as you could have your root AppView react to the changes on the AppState. It would however still need you to add a property to each child state and then a significant amount of boilerplate in your app state to derive the child state from the shared state.

Another problem with this approach is that whilst you can only have one alert at a time, you may want those alerts to behave in different ways but it the AlertState can only be generic over a single ActionType.

Is there a better approach that I'm missing?

Hi I had a similar problem and think I found a workaround for my project. In this thread Steven mentions holding a reference to a ViewStore in Scene/AppDelegate

I then created a static method on my Alert Action enum that I can call in any reducer.

enum SKAlertAction: Equatable {
    case timerTicked
    case dismiss
    case show(SKAlertContent)
    
    static func showGlobal(_ content: SKAlertContent) {
        let sceneDelegate = UIApplication.shared.connectedScenes.first!.delegate as! SceneDelegate
        sceneDelegate.globalViewStore.send(.alert(.show(content)))
    }
}

I can call it my reducer here:
SKAlertAction.showGlobal(SKAlertContent(title: "No login found.", style: .error))

You could do it that way, but I'd be hesitant to introduce a dependency on the global UIApplication.shared in this way. You're instantly making the code harder to test and harder to modularise.

As it is, I think I've found a good way of allowing entirely separate domains communicate without knowing each other using an Effect and a PassthroughSubject. I started a thread on this on the repo discussions, but you can see an example of what I came up with here:

This example uses the idea for some kind of global error handling but it could easily be applied to some kind of centralised global alerting/banner system - in fact I'm planning on refactoring our state-driven banner system to use this and greatly improve the ergonomics.