This article was originally written by the author in Better Programming on Medium.
Original article: SwiftUI: Choosing an Application Architecture – It’s hard to decide if you don’t know what you need.
SwiftUI: Choosing an Application Architecture
It’s hard to decide if you don’t know what you need.
SwiftUI’s declarative approach to software development makes it much, much easier to write modern applications for iOS, the Mac, and for the rest of Apple’s ecosystem.
But SwiftUI is not without its own challenges and questions, and one of those questions is just what sort of architecture we’re supposed use in our brand spanking new SwiftUI applications?
It’s a question frequently asked and — perhaps to no one’s surprise — one that’s frequently answered. There are, in fact many, many, many articles and books and presentations on the subject.
Unfortunately, quite a few of those are done by people eager to bring their own favorite architecture to SwiftUI. And so, we’re faced with an overwhelming torrent of articles telling us why we should use MVVM, or React/Redux, or Clean, or VIPER, or TCA. Or even none at all.
I suppose I’m as guilty as anyone in that regard, given the articles I’ve written on the subject myself.
Nevertheless, the question remains: Which one should we choose?
How do we decide?
What criteria do we use to make our choice?
What criteria… indeed?
You see, with that last question, a glimmer of light begins to appear on the horizon. Maybe, just maybe, it’ll be easier to answer our architecture question if we know the answer to another question:
What problems do we want our architecture to solve?
Defining the Problem
Physicists have an old saying: time is the thing that keeps everything from happening all at once.
And since time exists, since everything doesn’t happen all at once, we as a human race decided to create a few units of measurement in order to better keep things organized.
Things like minutes and seconds. Distinct periods of time that we further aggregate and group into hours and days, months and years, and so on. Common units, a common framework we can use to discuss time. To manage it. To understand it.
Similarly, while we could write our applications as one single massive block of code, we as software developers have chosen not to do that. Instead, we attempt to break our applications down into smaller units that are more easily understood. We try to write classes and structs and functions with well defined roles and behaviors and responsibilities. This is a View. That is a Model. This code over here is a Service which provides Models. And so on.
Application architecture basically boils down to the rules we use to decide how we split up our code into all of those individual components. Why that part of it goes there and why this part goes here.
Over the years we developed a few guidelines to help us out. Ideas like the single responsibility principle, the open-closed principle, the dependency inversion principle, and separation of concerns.
And if that wasn’t enough, we’ve even piled on a few more for good measure, high-level concepts like single source of truth, functional programming and unidirectional data flow.
Some of these ideas play well together. Others can add additional complexity and actually make even a simple program much harder to write, understand, and maintain. And some are little more than well-meaning attempts to impose constraints and issues found in other languages and on other platforms on Swift and iOS.
An application architecture — any application architecture — tries to codify and balance all of those rules and guidelines into a set of best practices we can use and follow.
And if we’re successful in our endeavors, if we’ve chosen our components and our architecture well, we end up with a functional, elegant, well-understood piece of software.
So, if we were to define an ideal architecture for SwiftUI… just what do we want from it?
Performance and Compatibility
If someone were to ask me that question, one of my first criteria would be that it needs to work well with SwiftUI itself.
In addition to defining our layouts with a simple, declarative interface, SwiftUI brought another other major concept to the table: State, and the idea of a single source of truth for a piece of data.

WWDC’19 “Data Flow Through SwiftUI” presentation
Update that state and the program automatically displays the change, reconfiguring and animating itself in the process.
Some people, however, take a single source of truth to the extreme. If there should be a single source of truth for a given bit of data, they ask, then why not have a single source of truth for the entire application?
This is the foundation of architectures based on the concepts behind React/Redux, and while the concept seems logical at first, it’s not without its flaws when applied to SwiftUI.
I wrote about this at some length in Deep Inside Views, State and Performance in SwiftUI, but the bottom line is that SwiftUI can be very efficient at determining the ramifications of a change to any given piece of data, and it will only redraw the portion of the interface affected by that change.
Create a single global state, however, and we can start to run into performance issues in larger applications where a change to that state requires SwiftUI to check and/or rebuild every dependency in the entire view tree (i.e. the entire application), each and every time any change occurs.
The alternative, as pointed out during Apple’s Data Flow Through SwiftUIpresentation, is to bind state as low in the view hierarchy as possible.

WWDC19 “Data Flow Through SwiftUI” presentation
When we bind low in the hierarchy, we dramatically minimize the number of interface updates and renders needed as only small portions of our view tree are affected by any given state change.
Another drawback to a large global state is that if you import that state into a single view then everything is exposed for everyone to see. As such, how can you be sure just what information a given view may be accessing or manipulating without walking through every line of code in the view?
Not good.
You can write additional code to filter the state into something suitable for the view in question… but then you’re basically writing more code in order to solve a problem you created for yourself.
A situation that’s somewhat less than optimal.
I highly suggest you read the Deep Inside Views, State and Performance in SwiftUI article if you’re interested in what’s going on behind the scenes in SwiftUI, but in the meantime I think we can use the above issues to rule out those kinds of architectures.
So, what’s next?
Simplicity
SwiftUI brings a simple declarative approach to writing applications. Perhaps more to the point, it’s concise. You can get a lot of work done with very little code.
As such, it would be a shame to burden ourselves and our applications with overly formal architectures that once again dramatically increase the amount of code we need to write.
Especially boilerplate code. It may just be a personal preference, but I hate boilerplate code. Further, I’m also of the opinion that the more code tends to lead to more bugs, as more code gives the little suckers more places to hide. And since boilerplate code is, well, boilerplate, it tends to be copied and pasted a lot in order to avoid retyping everything all over again… which in turn can lead to sneaky little copy/paste errors in your code.
This, to me, tends to rule out overly formal architectures like VIPER with its insistence on breaking each and every part of our application down into Views, Interactors, Presenters, Entities, and Routers. The VIPER architecture is heavily based on the single responsibility principle from SOLID, but from my perspective tends to carry that idea to an extreme.
VIPER was traditionally paired with UIKit-based applications where we had an unfortunate tendency to create a single large UIViewController for every screen in our application. Making distinct views and XIBs and nesting view controllers was often finicky work, and so all too often we didn’t bother doing so.
As such we looked for a way to move as much of the logic and user interactions out of the view controller as possible… and we did, each one into its own little individual part of the puzzle.
VIPER creates a lot of little moving parts, and so a significant percentage of the code written to manage VIPER is written to, well… manage VIPER.
Still, doing as much as we can to conform to the single responsibility principle has merit. Fortunately for us, SwiftUI has our back on that one…
View Composition
If one thing was repeated over and over again during the various SwiftUI sessions at WWDC, it’s that SwiftUI views are extremely lightweight and that there’s little to no performance penalty involved in creating them.
So, in SwiftUI it’s to your distinct advantage to create as many distinct and special purpose views as your app may require.
I’ve written extensively on the concept, in Best Practices in SwiftUI Composition, and again in View Composition In SwiftUI, so if you want to know more I suggest you add those articles to your reading list.
But the key takeaway here is that if we build our application with smaller views and view components, then we simply do not need the additional complexity typically generated by solutions like VIPER.
Testing
It’s possible to swing the pendulum too far, however, and decide based on everything said so far that we don’t need any architecture at all. Just cram all of our code into a bunch of little views and be done with it.
Consider the following view.
struct OrderDetailsRowView: View { var item: OrderItem var body: some View { HStack { if item.quantity == 1 { Text(item.name) } else { Text("\(item.name) $(\(item.quantity, specifier: "%.2f") @ $\(item.price, specifier: "%.2f")") } Spacer() Text("$(\(item.total, specifier: "%.2f")") } } }
This is a great example of a small, dedicated view in SwiftUI. Well, great, perhaps, other than the fact that all of the logic and formatting is crammed together inside the view body.
Which makes it extremely difficult for us to write test cases that ensure that the output of this view is correct.
Separation of Concerns
The leading architecture often proposed is MVVM (Model-View-View Model).
That said, it really should be written as Model-View Model-View (MVMV), since the View Model exists to mediate between your application’s data (the model) and the requirements of the view (the layout).
With a View Model, we want to move as much of the logic out of the view as possible, leaving behind code that’s dead simple to understand.
That’s a good example of separation of concerns. We put our business logic on one side of the fence, and all of our view presentation and layout on the other.
This might be better illustrated with an example, so let’s consider the following SwiftUI View that’s the parent of our earlier OrderDetailsRowView
view:
struct OrderDetailsView: View { @StateObject var vm = OrderDetailsViewModel() var body: some View { Form { if vm.message.hasMessage { StatusMessageView(type: vm.message) } LabelValueRowView(label: "Order", value: vm.dateValue) ForEach(vm.items) { item in OrderDetailsRowView(item: item) } if vm.hasDiscount { LabelValueRowView(label: "Subtotal", value: vm.subtotal) OrderDetailsDiscountView(value: vm.discount) } LabelValueRowView(label: vml.totalLabel, value: vm.total) Button("Order Again") { self.vm.reorder() } } .onAppear { vm.load() } } }
Note that everything in this view is driven by the view model.
All of the conditional and computed values come from the view model. There are several if
statements controlling the visibility of certain elements, but again, the logic behind those decisions is made in the model. The view simply carries them out.
When the state changes, say by hitting the “Order Again” button, the view again regenerates based on the view model
So, if we test our view model, and if we see the desired output, and if our view controller is correctly bound to our view model, then we can say with a fair degree of confidence that our screen — and our code — is correct.
Testing Dedicated Views
While I personally consider MVVM to be well suited to SwiftUI, it’s possible that even it is overkill in some cases. Not every view needs a distinct view model.
We could, for example, refactor our original detail row view to look like the following.
struct OrderDetailsRowView: View { var item: OrderItem var body: some View { HStack { Text(itemDescription) Spacer() Text(itemTotal) } } var itemDescription: String { if item.quantity == 1 { return item.name } else { return "\(item.name) (\(item.formattedQuantity) @ \(item.formattedPrice))" } } var itemTotal: String { item.formattedTotal } }
Given this view body, it’s easy to understand that we’re displaying two pieces of data, and there’s simply not much there that can go wrong from a business logic perspective.
And with the conditional logic and formatting broken out into distinct variables it’s possible to instantiate the view itself with a single item and test our logic to see if it’s correct, and then make another one with two items and test that.
func testOrderDetailsRowView() { let view1 = OrderDetailsRowView(item: OrderItem.mock1) XCTAssert(view1.itemDescription == "Soft Drink") XCTAssert(view1.itemTotal == "$1.99") let view2 = OrderDetailsRowView(item: OrderItem.mock2) XCTAssert(view2.itemDescription == "Cheeseburger (2 @ $4.99)") XCTAssert(view2.itemTotal == "$9.98")}
Again, we simply need to move as much logic as possible out of the view body and put it into variables and functions we can see from outside the view.
Because, and to paraphrase a military axiom: If we can see it, we can test it.
That said, if a given view started to get too much larger, I’d start considering how to either break it apart into smaller views or else I’d start moving my conditional code and formatting out into a dedicated view model.
And if a given view needed to handle API requests handle errors and error messages, manage edge cases like empty lists, and so on, then I’d definitely move to a dedicated view model.
For more on correctly setting up VM’s with network request logic, see: Using View Model Protocols in SwiftUI? You’re Doing it Wrong.
Criteria for a SwiftUI Architecture
To sum up, my criteria for choosing a SwiftUI architecture is as follows…
- It must be performant, no matter the application size.
- It must be compatible with SwiftUI behavior and state management.
- It should be concise, lightweight, adaptable, and flexible.
- It encourages SwiftUI view composition.
- It supports testing.
In other words, it lets SwiftUI be SwiftUI.
Completion Block
It might appear that I’ve taken you by the hand and led you down the path to stand before the altar of MVVM (and, to be fair, that’s exactly what I did). But at least we now know why we’re there in front of it, based on the choices we made.
Do you agree with my criteria? Got some of your own? Agree with my conclusions or did I miss something? Either way, I’d like to know, so drop me a line in the comment section below.
Want more interesting stories? Feel free to check out the other articles in my SwiftUI Series.

I write about Apple, Swift, and technology. I’m a Lead iOS engineer at CRi Solutions, a leader in cutting edge mobile corporate and financial applications.