This is the first in a series of posts about SwiftUI and the progress I’ve made so far with it. Before we get started, however, a few caveats:
- SwiftUI is still a 1.0. To be fair, that is one hell of a 1.0 and such a great job by Apple. But as of the writing of this post, WWDC 2020 is only a month away and we can assume at least a SwiftUI 1.1 but probably more like a SwiftUI 2.0. That might make this entire series outdated.
- I’ve been progressing slow because the main app I’m working on with SwiftUI isn’t something I am working on full time. I’ve also been progressing slow because I’ve changed my mind several times about how I think it should be structured. So what you are getting at this point is where I am at today, it could change tomorrow.
- There are many different takes and ways to tackle the problems I will list out in this post. What I’m going to blog about is only one of them and isn’t a comparison between architecures. Actually my point in these posts is to explain my thinking and get you to think also.
With that out of the way we should talk about some high level app architecture concepts that all apps should consider. As you will see, only a couple of these applies only to SwiftUI, but we will be looking at these in the context of a SwiftUI app. The concepts and thought process, however, are universal and can be applied to many types of apps, not just ones for Apple platforms.
The way I like to think about app architecture is both top down and then bottom up. From the top I like to set out some themes and overall goals of what I would like my app architecture to accomplish. Then I use those as guiding principles while building the app from the bottom up, refactoring and changing things as I go. Yes, this can be a slow process sometimes. But the more you do this, the more confident you get in your previous app archiecture choices and the quicker it becomes. Ultimately this is a continually evolving process and there is NO app architecture that works for every app, every team, every developer. Just toss that idea out the window. It is also possible to build an app with no thought at all about app architecture. Eventually you will get to a point where adding new features is very difficult and changing anything in the app will cause unexpected side effects. So thinking about app architecture and constantly refining it is the key to maintaining a codebase for a very long time.
Our top down architecture principles
Here I want to discuss some of the top down principles that I strive to achieve in all my apps. Again, this is not a comprehensive list, just a set of my current top principles I like to focus on. Each one of these will get a blog post of it’s own, but for now we can set some basic guidelines on each.
Views constructed with a component mentality
This is where SwiftUI shines. Breaking down your apps UI into very small components provides for consistency and speed in development down the road. Several of the following principles need this one in place to be fully sustainable. Breaking down views into very small and simple components, then building each feature in the app using these components is like making your own LEGO blocks and then building a model from it. You do NOT need to figure out all the components ahead of time either. You can do this as you go along. Eventually your project will have a library of pieces that you can build on top of very quickly. This also lets you change the entire UI of the app by changing some of the small building blocks that you have created.
Views should only focus on the visual and should be backed with a view model
This is really the foundational principle in the MVC architecture and many others. At the basic level this is simply saying that a view should have no knowledge of how it fits within the application structure and flow, but is only concerned with it’s own visual representation. If we have based our app architecture on view components, this becomes even more powerful. With components, each views responsibility is very small. Small amounts of responsibility equals maintainability. And this way of building apps can be fast. Don’t get hung up here with the thought
what is a view model? Keep this idea high level and simple. A view model is the data model that contains the state that the view needs to display. Simple as that. When the view model changes the state, the view should update the visual display. With SwiftUI, using an
@ObservedObject provides just what we need.
Understand all the different states your view has and make it easy to see them
Again this is an area where SwiftUI shines. The Xcode preview for SwiftUI is amazing! I highly recommending using it to it’s full potential. Also, this builds strongly on the previous two principles. With simple component views that are backed by view models, we should be able to easily setup previews for each view and all the different states it can be in. A very high percentage of bugs in mobile apps are due to the view not displaying the correct state of the data. By showing these different states in Xcode, developers can quickly identify problems. I don’t think people understand yet the maintainability power you get once you have rich previews of your views in place. Once you go back to that app after months or years, the previews will help you figure out what the app does without even running it!
Connect views using features and routes
Because we don’t want views to know where they exist in the application structure and flow, we need to have something that understands that! This is where features and routes come into play. Again we should keep things simple here. A feature is simply a combination of some specific data source and one or more views that will display that data (or you can call this a controller or a coordinator). With this in place we can show different sources of data using the same views. Features are usually lightweight as their job is small, just get the data and tell the view(s) to display it. We will go into much more detail, but needless to say using Combine is excellent here. And going one more step up in the app architecture, we need a way to route between features. We shouldn’t include that information inside the features themselves, but at a level above them. Once you need to deep link to a feature from a notification, you will be happy you have a routing system.
Unit test all logic
Unit tests are for logic. We can use them for lots of things, but their primary use is to test all the logic paths in the app. Make sure unit tests are fast and do not call any real services. Make sure the unit test suite is robust and not fragile. Get to 100% code coverage in most of your code if you can (you should be able to), but don’t write bogus tests just to achieve a high code coverage number.
Build UI automation to test flows
While unit tests are good at testing logic, they aren’t as good at testing if the application really works. You would be surprised how much of your code you can test with a simple UI automation flow test. Also a basic UI automation test suite provides hidden value such as showing how the app is supposed to work. Don’t get into too much detail here or repeat what unit tests should be covering. And don’t use real services for your UI automation. That is just asking for an unreliable automation suite.
Test UI regression using snapshots
Unit tests are pretty terrible for testing views. With SwiftUI that is particularly made more difficult because you don’t have direct access to the view elements. While there are UIKit/AppKit views behind the scenes, trying to access them is fragile at best. We should rely mostly on our unit tests and UI automation suites to test that the app is doing the expected things. However one valuable technique to understanding the impact of a change is to perform snapshot testing. This is where you take a baseline snapshot of the app visually, then whenever you want you can compare a new snapshot to this baseline. The difference between the two will tell you at a glance what all has changed. This gives you an easier way to check to be sure the change you intended to make has been accomplished, while exposing any unintended side effects of your change.
With only these 7 principles in place you can build a very robust codebase that is testable. In the rest of the series we will walk through each one and show ways to accomplish them. But again the point here is not that these are the same for everyone, or even a complete list. The point is just for you to decide what your foundational principles are going to be as you develop your app and those principles will drive your design choices. For example you might choose to have
Everything should be optimized for speed as one of your principles. Choosing that as one of your principles will lead you in a far different direction in your app architecture as you can imagine. But it is totally appropriate for certain types of applications. For me architecture isn’t about finding the architecture first. It is about finding the principles first and then letting the architecture grow organically from that. Then your app architecture will fit your codebase like a glove, rather than you trying to fit your hand in an existing glove.