Adapt Unidirectional Flow Virtues to Your Plain SwiftUI App

You can adapt principles of unidirectional flow without having to adopt a whole framework like ReSwift or Composable Architecture or Immutable Data.

I find the flow of actions through reducers to change a state and produce effects easy to reason about, even after years of neglecting a code base. The principles behind that can enrich your programming life, too.

Here’s one such idea.

For example, a unidirectional flow architecture with a central store makes it easy to filter actions users are allowed to do in one place, and produce effects like requiring login. With vanilla SwiftUI, these things become ad-hoc decisions: “I have this button right here, right now, how do I show a log-in form?” – There’s ways to rethink the local problem as an app-global one, because it’s a core business logic to not permit some interactions unless a condition like user log-in is met:

  • From the idea of a central store, you can adapt the idea to take care of this in a unified manner early on in the app’s structure.
  • From the idea of an action reducer, you can adapt the idea to spell out your actions and their filters, making these concepts explicit types (or functions).
  • From the idea of an action dispatcher, you can adapt the idea to inject handlers for complex flows into the environment.

So take a type that you can inject into the environment similar to the built-in SwiftUI.DismissAction:

struct EnforceAuth {
    var isLoggedIn: () -> Bool
    func callAsFunction(_ block: () -> Void) {
        if isLoggedIn() {
            block()
        } 
    }
}
// register .enforcingAuth environment key

Then early on in your app, inject the handler so that you can produce a side effect via a local state to how a login overlay/sheet/…:

@State private var showsLoginSheet = false

MyView()
    .environment(\.enforcingAuth, EnforceAuth { 
        let isLoggedIn = ... // check auth
        if isLoggedIn == false { showsLoginSheet = true }
        return isLoggedIn
    })
    // bind @showsLoginSheet to .sheet(...) or .fullScreenCover(...) etc.

The call sites can then wrap their actions with this anywhere, no matter how far away from the root of the view hierarchy:

@Environment(\.enforcingAuth) private var enforcingAuth

Button("Do something", action: { 
    enforcingAuth {
        deleteInternet()
    }
})

Escalating: Typed Knowledge

If this pattern proves to be useful, there’s potential in making sure that you cannot forget to enforce the authentication check for certain actions. You can start strongly typing your actions and shift the burden of remembering to authenticate from N call sites to 1 type declaration that clearly signals this.

You would implement DeleteInternetAction, and annotate on the type level that this is AuthRequired. In the vein of parse, don’t validate, the enforcingAuth then will take the action value but refuse to execute the action sequence unless the user is logged-in.

Escalating: Central Processing

Finally, so that you don’t forget to annotate your types, you could start to use a centralized action reducer and implement that part of the business logic there. Maybe in a separate package target, even. Then you can think of what kind of user behavior requires authentication in one place and have this close together.


This pattern can be used in your SwiftUI app for isolated problems like requiring user authentication and mixes well with all your observable models and whatnot to handle state and logic elsewhere. Unlike a proper unidirectional flow library, it’s not intrusive. But maybe the brain worm that comes with this is, and you get inspired to check out different ways to model your app’s state and action handling in the future, SwiftUI or UIKit or Qt or whatever.