SwiftUI App Architecture

  • Separation of concerns, so multiple developers know and understand what they are working on.
  • Having SwiftUI Previews with mock data to efficiently develop and review UI, sometimes directly with designers and product owners.
  • Having an architecture which is suitable for Unit Testing and UI Testing so we can have a high Code Coverage of our app and thus ensure correctness of our software.

Our architecture for SwiftUI/Combine apps

Simplified overview of our MVVM architecture for SwiftUI and Combine apps.

Views layer

  • @State private var: when the state won’t leave the View or its sub-views. This is typically for information which is only UI relevant, not persisted and has no impact on the logic of the app. Example: the expanded state of an accordion widget.
  • @Binding var: for sub-views referencing the state of the parent View. The View does not “own” this state but is updated/updates the value owned by a parent View. Example: a custom checkbox View.
  • @ObservedObject var: the state is not only UI relevant, it is part of the logic of the app, but it is scoped to a specific part of the application. The @ObservedObject is generally not owned by the View itself, it is received from its parent. ObservableObject are recreated every time the parent View is recreated. Example: states of a specific screen in the app.
  • @StateObject private var: @StateObject were introduced with iOS 14. They are pretty similar to @ObservedObject in the way that they are intended for states that are not only UI relevant and scoped to the View and its subviews, but the ObservableObject is not recreated every time the view is displayed. So, when choosing between @ObservedObject and @StateObject, there is a tradeoff between computation time of recreating the ViewModel every time and keeping it in memory all time long. Example: states of a specific screen in the app.
  • @EnvironmentObject var : @EnvironmentObject are ObservableObject injected in a View hierarchy using .environmentObject() modifier. Then the entire hierarchy can conveniently retrieve them with @EnvironmentObject. This is intended for states which are not only UI relevant, and not scoped for a specific part of the application. We typically inject the ViewModels in the SceneDelegate on the entire View hierarchy of the App. When choosing between @EnvironmentObject and @ObservedObject/@StateObject, there is a tradeoff between having states easily available but widely exposed or having states less conveniently available (pass it through initializer in the hierarchy) but scoped to where it must be used. Note that @EnvironmentObject need to be passed again (by using .environmentObject()) explicitly when using NavigationLink or .sheet().

ViewModels layer

Typical implementation of a ViewModel.
Typical ViewModel protocol.
Typical fake ViewModel implementation.
Error while adopting the ViewModel protocol directly.
Correctly adopting the ViewModel protocol using generics.

Models layer

  • Publisher: which will stream events up the layers.
  • func: to triggers event down the layers.
  • Publisher or Subject: to stream events down the layers.
Typical Model implementation.
Typical Model protocol with erased type Publisher.


Detailed architecture overview

Detailed architecture overview.

Why does this design pattern fit our needs ?

Separation of concerns

  • Views are responsible for describing how the UI looks like.
  • ViewModels provide the content for the Views and handle what to do in case of UI events (e.g. a button is touched).
  • Models contain app business logic and interfaces to external components (Networking, Libraries, Persistence, etc.).
Each layer can be tested individually.

Unit Testing

Real and fake Model implementation are both implementing the same protocol.
Unit test of ViewModel relying on a fake Model implementation.

SwiftUI Previews with fake ViewModels

Previewing the same View with different FakeViewModel states.
SwiftUI Preview based on fake ViewModel implementation.


UI Test triggering scenario 1 based on fake ViewModel implementation.
SceneDelegate implementation: using pre-compiler flags and launch environment variable we determine the ViewModel implementation to use.




Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store