A NiceArchitecture for SwiftUI

Apr 5th, 2024 • Releases • Brendan Lensink

Since its introduction at WWDC in 2019, SwiftUI has powered an exciting step forward in building native UIs.

In the years since adopting SwiftUI, we’ve launched and maintained multiple quality apps driven entirely by SwiftUI, such as Ridwell, and written our own suite of SwiftUI components. We’ve also been iterating on and improving our architecture to get the most out of SwiftUI.

We’ve seen a lot of success with our own flavour of MVVM, tweaked just a little to suit SwiftUI. We’ve found this results in a codebase that’s well organized, familiar to developers across different disciplines, and highly testable without needing a lot of initial set up or any big paradigm shifts away from the architectures developers likely know and use today.

We like to call it NiceArchitecture.

Situation: There are 14 competing standards

We started, as we normally do, with some goals.

  1. Minimal reliance on large dependencies or really novel concepts. We prioritize writing focused apps that we can hand back to clients and be confident they’ll be able to go in and make their own changes in the future, without committing them to an architecture they might not be fully bought into. Higher commitment architectures, like The Composable Architecture, may have advantages for apps with lots of client-side state management. However, they also impose a major non-system dependency and require substantial discipline to keep straight.
  2. Highly testable. Testable code is great for catching regressions before they happen, and generally makes it easier to make changes to the app without having to worry about unintended side-effects.
  3. Conceptually similar to what our Android team is doing with Compose. Keeping our two architectures similar to each other lets teams that work on both platforms apply lessons learned in one place to the other, and makes it a little easier on devs to swap between projects.

From past experiments, we knew any architecture would need to tackle the problem of being able to navigate to arbitrary screens on launch, for example when a push notification is opened. Implementing this with vanilla NavigationViews can be challenging in SwiftUI.

A diagram illustrating the challenges of trying to navigate to arbitrary screens in a mobile app.

With these goals in mind, we went off and developed a pair of demo apps, one for iOS and one for Android, to use as a proving ground for a modern SwiftUI architecture and provide a blueprint for future projects.

Since then, we’ve successfully launched numerous projects using this architecture, and iterated and refined the original ideas into the architecture we describe here today.

A Nice Little Architecture

For the most part, NiceArchitecture sticks with the standard MVVM concepts most mobile developers are familiar with like ViewModels, Repositories, and Services. But it also adds in a little spice with the concept of ViewCoordinators to handle navigation, and opinions on things like Dependency Injection and how to manage a screen’s load state.

A diagram showing the various parts of a mobile app and how they connect.

For a deeper dive into each of these concepts, check out our example project on GitHub

ViewCoordinators aren’t conceptually difficult – they’re roughly analogous to the old UINavigationControllers – but they do have two pretty nice benefits:

  • ViewCoordinators keep View code simple and focused on UI. They eliminate the need to worry about overlays, sheets, NavigationViews, etc., allowing Views to focus solely on displaying their data.
  • ViewCoordinators allow for quick navigation to nested views. Since the coordinator is responsible for all of its child View’s navigation, it’s able to handle things like opening straight into a nested detail view easily.

Building Strong Fundamentals

We’ve had a lot of success with NiceArchitecture, and we like that it embodies a number of iterative lessons we’ve learned building SwiftUI apps in production based on a strong foundation in MVVM. We think a big part of its success is based on the helpers and conventions we’ve established to make the whole thing run smoothly and build upon that starting point.

While these utilities certainly aren’t necessary to use these patterns, they provide compelling default behaviours for SwiftUI apps, and we find they’re a major accelerant for prototyping apps and getting new apps up to a robust UX quickly.

You can see some more examples in our deep dive, but here are some quick highlights:

Dependency Injection

A big part of testability is being able to swap out different pieces of your architecture for mocked versions. This fantastic article describes how we’re able to bring the benefits of DI our Android developers know and love over into Swift-land, making switching out our Repositories and Services for mocked versions a breeze.

ContentLoadState

Keeping track of a View’s internal state is key to a good user experience - few things are more frustrating than being stuck on a screen without knowing what’s going on.

To make this easy, our ViewModels publish a simple struct called ContentLoadState that is responsible for keeping track of what’s going on behind the scenes and making sure the View is always reflecting the current state of things, whether that’s an error that has just occurred, some data that’s still loading, or the final view once everything’s loaded.

Observable ViewModels

Our ObservableVM class, which all subsequent ViewModels should extend, provides a bunch of conveniences that make adding new ViewModels a breeze and ensures you don’t miss any steps. Things like binding your View to your ViewModel, tracking your ContentLoadState, and tracking and displaying errors are handled behind the scenes, leaving your ViewModels to focus on the important things.

To Infinity and Beyond…

If your team works in SwiftUI, or may be embarking on a new SwiftUI project soon, we hope you’ll find these approaches helpful in making your own architectures maintainable and awesome. SwiftUI has made it easier than ever to create polished, well-organized UIs – plus no more waiting an eternity for Storyboards to open. For a deeper dive into NiceArchitecture, including a detailed example project and supplemental package, check out our GitHub.

If you found this helpful, or have any of your own SwiftUI architecture strategies, we’d love to hear from you. We’re always looking to improve and can’t wait to see where Swift brings us next. 🚀

Brendan Lensink • Product Developer

I just think it's neat.