Architecting SwiftUI Apps With MVC and MVVM
Architecting SwiftUI Apps With MVC and MVVM
If you will think I did a good job, I ask you to share this with someone
you think can bene t from it.
That way, I can spend less time looking for people I can help and more
producing great free material. You will nd some links to share this
guide at the end. Thank you.
Let’s start.
fi
fi
If you are just starting to make iOS apps, most of your questions
probably revolve around how to structure your code. You might al-
ready know some of the features of iOS, but you can't bring them all
together in a well-structured app. So, you are going to get the most
value out of this guide.
And even if you already have some experience, I am still sure you can
get a lot of value out of this guide as well. Online articles, books, and
courses rarely approach iOS development from a structural point of
view. App architecture is always an afterthought, and many developers
get bad habits. This guide will hopefully correct those.
I have been teaching the concepts you'll nd in this guide for years,
and I have recently updated them for SwiftUI apps. I have noticed that
many experienced developers don't know them. At the beginning of
my career, I didn't know them either and made most of the mistakes I
now see in many apps.
4
fi
When you learn to program in Swift, you get a plethora of tools: func-
tions, structures, enumerations, classes, protocols, and many others.
What you don't learn, though, is how to use them and for which pur-
pose.
There are many tasks you need to consider when building an iOS app.
The most common ones, which you nd in pretty much any app, are:
• Representing data.
There are many ways to address these points, using the constructs of
the Swift language. But that does not mean that every approach is cor-
rect.
fi
Some developers like to say that the architecture of your app depends
on the speci cs. I disagree. Sure, you have some exibility, but if you
take it too far, you will get a house that collapses under its weight.
There are many architectural design patterns that tell you how to structure
your apps, all with different, esoteric names. Following any pattern is
better than not following any pattern at all. But, if you look at them
closely, you will notice they all have the same structure, like houses.
The MVC pattern divides the structure of any app into three layers,
each with its precise responsibilities:
• The model layer represents the data of the app and encapsulates
the domain business logic.
• The view layer is the user interface of the app. It shows data to
the user and allows interaction.
• Finally, the controller layer acts as a bridge between the other two
layers and implements the app’s business logic.
(I'll explain you later the difference between the domain business logic and
the app's business logic).
6
fi
fi
fl
In just a paragraph, I gave you a blueprint you can follow to build any
app. Don't worry about the details. We will spend the rest of this guide
exploring code. What is important is that now you know where, ap-
proximately, each new piece of code goes. These roles will guide every
structural decision you make.
What I showed you above is the “vanilla” version of the MVC pattern.
For decades and to this day, it is used by developers on many plat-
forms. So it's not a surprise that it works for SwiftUI too.
iOS apps are made of many "screens" through which the user can
move. That requires code to handle navigation and to map the user
actions into the app’s business logic. As an app grows, it becomes clear
that all that code concentrates at the meeting point of the view and
controller layers.
To avoid large intricate code and keep our app well structured, we can
add an extra layer to take those responsibilities. In UIKit, that was the
role of view controllers. In SwiftUI, that code does not magically disap-
pear, so we need a similar layer. I named it the root layer because it's
composed of views that sit at the root of the view hierarchy for an en-
tire screen.
7
fi
fi
Why the arrows follow those directions will be clearer when we will
look at the code for our app. What is important, for now, is the four
layers and their responsibilities.
In short: the MVC and MVVM have always practically been the same
design pattern, with a few nuanced differences. In SwiftUI, those go
away.
That's is all you need to know, so you can skip this section.
For those who are unconvinced, or that want an explanation, here are
some more words.
You don’t need to be a genius to see that the view model layer of
MVVM is the controller layer of MVC. The only difference is that the
view and view model layers are connected through a binder, using some
declarative data binding technology instead of standard programming
practices like it happens in MVC.
That is, or better, was, the only difference. In UIKit, MVC apps used
the lifecycle events of view controllers, delegation, and callback clo-
sures to transfer data between the view and controller layers. MVVM
apps, instead, relied on open-source event-driven functional reactive pro-
gramming frameworks like RxSwift.
That difference goes away in SwiftUI. The only way to transfer data to
the view layer is by using observable objects with @Published properties.
SwiftUI views connect to these objects using the @ObservedObject
and @EnvironmentObject property wrappers.
We have come full circle back to the diagram of the extended MVC
pattern. The arrows are slightly different, but is also depends on which
chart you read. In any case, they are far less important than people
think.
For these reasons, I will only talk in terms of MVC, which is the origi-
nal pattern. Its variations are irrelevant, and sometimes, even worse.
10
MANAGING DATA AND BUSINESS
LOGIC IN MODEL TYPES
I didn't spend too much time on a pretty design because it's not essen-
tial. This will also keep the code as small as possible, so we will be able
to focus on the app's structure without extra distractions.
I won’t explain the code in the app line by line either, as I usually do in
my articles and courses. Instead, I will show you the relevant sections
of the complete code, which you can download here. The point of this
guide is to learn how to structure apps, not learning every little detail
of Swift and SwiftUI.
fl
networking, I have a full course called The Con dent iOS Professional
that covers all that, and much more. I open the course regularly only
to the subscribers of my email list, so be sure to stay in it (you can join
here).
The model layer is the foundation of our app. Many developers like to
start from the app's user interface, and that's a ne approach. But I of-
ten prefer to start with model types.
The model layer is independent from the rest of the app. Model types
only focus on data and don't need to worry about storage, networking,
or the user interface. This makes them easy to write and test.
Our app needs to deal with bank accounts and transactions, so we can
create a couple of types to handle those.
struct Transaction {
let amount: Int
let beneficiary: String
let date: Date
}
struct Account {
let name: String
let iban: String
let kind: Kind
var transactions: [Transaction]
}
extension Account {
enum Kind: String, Codable, CaseIterable {
case checking
case savings
case investment
}
}
fi
fi
fi
signi cant source of bugs in any software. With value types, we keep
our code as independent as possible.
(Side note: I used Int for the amount property of the Transaction
type instead of a Float because you should never use oating-point
numbers for precise numbers, like in nancial transactions. Money
amounts are usually expressed in cents, using integers).
The most signi cant mistake developers make is putting the domain
business logic in the other layers of MVC.
The domain business logic is any logic that deals with the domain cov-
ered by our app and its rules. In our case, the eld is banking. So, all
the code that deals with the laws of banking belongs to the domain
logic.
Many developers stop at the code above, leaving the model layer too
thin. They use model types to only represent the data in their apps.
But in MVC, the model layer should take care of much more. Model
types should also:
13
fi
fi
fi
fi
fi
fl
Since these rules belong to the domain business logic, their code
should also go into the model layer.
struct Transaction: Identifiable, Codable {
let id = UUID()
let amount: Int
let beneficiary: String
let date: Date
}
extension Account {
enum Kind: String, Codable, CaseIterable {
case checking
case savings
case investment
}
}
Our model types are now richer and enforce all the rules I listed
above. For starters, the initializer of the Account structure sets the ini-
tial transaction to €2.000. (I'm simplifying here. The request to create
an account would usually go through the bank where the customer
would physically bring the sum).
14
Our model types also handle data transformation. All the types con-
form to the Identi able and Codable protocols, which allow us to
organize, store, and send data. If we had more complex rules for en-
coding and decoding our data, those would also end inside the
Transaction and Account structures.
Often, all this logic ends inside controllers or, worse, views. That's a
mistake. It spreads the business logic across the entire app, often dupli-
cating functionality and making it unmanageable. Keeping it inside
model types makes it simpler to write and to test. And since our full
app will rely on this business logic, this approach produces fewer bugs.
15
fi
fi
Moving up the MVC pattern, the next layer we meet is the controller
layer. Here we also nd many responsibilities that developers often put
inside SwiftUI views.
While the model layer contains the domain business logic, the controller
layer contains the app’s business logic, which includes:
• dealing with data and events coming from the device sensors
(GPS, accelerometer, gyroscope, etc.)
We will start with saving and reading data on disk, which many apps
need. For that, we will use the iOS le system, saving our data in the
documents directory.
class StorageController {
private let documentsDirectoryURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first!
16
fi
fi
The rst thing to notice here is that we used a class and not a struct.
Controllers need to be shared across the entire app and provide a
unique access point, so they need to be objects.
While this class does not do much, it is crucial to keep its code separat-
ed from the rest. In a real-world app, things are rarely this simple. Any
code in your app is likely to grow larger with time, so keep responsibili-
ties separated from the start.
Pretty much any app needs to store its global state somewhere.
Holding the app's state is another of those tasks that require more
code than it might be evident at rst glance. Many developers just
store the data representing the current app state in an observable ob-
ject and call it a day.
class StateController: ObservableObject {
@Published var accounts: [Account]
}
Whenever you see a type with stored properties but no code, you
should be suspicious. While that can sometimes happen, it's not des-
tined to last for long anyway.
Recall that the controller layer must contain the app’s business logic,
which de nes, among other things:
17
fi
fi
fi
fi
fi
First of all, we need a way to generate IBAN codes for new accounts.
This is another of those tasks that would typically be carried by the
bank. A real app would fetch it from the network, but for simplicity, we
will generate IBAN codes in our app.
extension String {
static func generateIban() -> String {
func randomString(length: Int, from characters: String) -> String {
String((0 ..< length).map { _ in characters.randomElement()! })
}
IBAN codes must follow a precise algorithm, but we don’t care here, so
I just generated a random string that looks like an IBAN. I put it into a
String extension because this is still part of the domain business logic.
We can now extend the state controller to perform all the tasks I listed
above.
class StateController: ObservableObject {
@Published var accounts: [Account]
init() {
self.accounts = storageController.fetchAccounts()
}
18
accounts.append(account)
storageController.save(accounts)
}
Notice how much code we have now. This is code that would have
ended inside views if we didn't gather it here.
For simplicity, that happens every time our data changes. This works
for small amounts of data since access to the disk can remain relatively
quick. In an app with a more signi cant amount of data, a more so-
phisticated policy might be required, for example, dispatching disk ac-
cess to an asynchronous background thread.
Slowly but surely, we are building our house, placing each new type on
solid foundations.
19
fi
fi
We will skip the root layer for now, and jump to the view layer. The
reason is simple: root views are a bridge between controllers and views,
so it’s easier to implement them when the other two exist.
Unlike what happened in other layers, here it's not important how
many, but how few responsibilities there are in views. The mistake here
is putting too much code inside views.
20
fi
fi
Before we go on, we need to create some data for our Xcode previews.
It’s a good practice to place such data in a separate structure, so we
don’t need to recreate it in every preview.
struct TestData {
static let account =
Account(name: "Test account", iban: String.generateIban(), kind: .checking)
static let transaction =
Transaction(amount: 123456, beneficiary: "Salary", date: Date())
}
That applies to any code, not only to SwiftUI speci cally. Whether you
write imperative or declarative code, extended functions are hard to
read and easy to get wrong.
When looking at how we should split our view code, some cases are
more obvious than others. In general, any view that repeats more than
once is a good candidate.
21
fi
The rows of a table are also another glaring case of reusable views.
We nd one instance of that in the Accounts screen.
extension AccountsView.Content {
struct Row: View {
let account: Account
I like to namespace my views inside their parent view using Swift ex-
tensions to avoid excessively long type names. The full name of this
view is AccountsView.Content.Row. But you don’t have to follow
22
fi
fi
this convention. For example, you can call this view AccountRow and
place it outside the Swift extension.
extension Date {
var transactionFormat: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: self)
}
}
extension String {
var ibanFormat: String {
var remaining = Substring(self)
var chunks: [Substring] = []
while !remaining.isEmpty {
chunks.append(remaining.prefix(4))
remaining = remaining.dropFirst(4)
}
return chunks.joined(separator: " ")
}
}
23
fi
extension TransactionsView.Content {
struct Row: View {
let transaction: Transaction
While reusable views are the most obvious candidate for separating
code, they are not the only ones. Often, a single screen has complex
sections with extended code. Even if you don't need to reuse such code
anywhere else, it's still good to put it inside a separate type.
24
First of all, it lays out the user interface for the entire screen and also
sets the navigation bar title. It also sorts the transactions by date so that
the most recent ones are at the top. This is part of the user interface logic
that belongs to the view layer. This does not in uence the global state
of the app in our StateController, which can sort and store transac-
tions in any order that makes sense there.
And nally, the Content view enables user interaction through the
New Transaction button. Here, it is also essential what our view does not
do. The Content structure does not decide what happens when the
user taps the New Transaction button. That’s part of the navigation
structure of the app, which belongs to the root layer. Instead, the ac-
tion for the AddButton is provided to the view by an ancestor
through the newTransaction property of the Content type.
25
fi
fl
.navigationBarTitle("New Transaction")
.navigationBarItems(leading: cancelButton, trailing: sendButton)
}
This view is not concerned with creating new transactions and does
not interact with the StateController in any way. It also does not
dismiss the screen when the user acts.
Keeping view code separate from the controller layer makes it also
more straightforward to create previews.
26
Since the Content view does not interact with objects, we can pass
simple values as parameters in the preview. Adding a Navigation-
View makes the navigation bar visible, so we can check that the whole
screen looks as desired.
Again, this view contains interactive views like TextField and Picker
and sets the navigation bar buttons, but does not handle any of the
app’s business logic.
28
fi
VStack {
List {
ForEach(accounts) { account in
NavigationLink(destination:
TransactionsView(account: account)) {
Row(account: account)
}
}
.onMove(perform: move(fromOffsets:toOffset:))
}
AddButton(title: "New Account", action: newAccount)
}
.navigationBarTitle("Accounts")
.navigationBarItems(trailing: EditButton())
}
This view contains some more user interface logic and allows the user
to reorder the accounts on the screen. But again, it does not interact
with the StateController where those accounts are stored. Instead, it
takes an array of accounts through a @Binding property, which it
uses to send data changes to an ancestor view.
29
The lower and upper layers of our app are still disconnected. What
brings them all together is the root layer, which we will cover in the
next, last chapter of this guide.
30
fi
We have nally come to the last layer of the MVC pattern: the root
layer. Beware that the naming is mine, so you probably won't nd any
other developer refer to it with this name. That is unless this idea
catches on.
31
fi
fi
fl
fi
This is our rst root view. Its code is entirely structural. It binds the
accounts property of the Content view to the accounts property
of StateController.
It also presents the New Account screen modally and provides two navi-
gation views. The rst one enables the main drill-down navigation for
the app, which leads to the list of transactions for the selected account.
The second navigation view is only needed to show a navigation bar in
the New Account screen, even though navigation does not proceed fur-
ther.
struct TransactionsView: View {
let account: Account
32
fi
fi
fi
All we have left are the views to create new accounts and transactions.
struct NewAccountView: View {
@EnvironmentObject private var stateController: StateController
@Environment(\.presentationMode) private var presentationMode
@State private var name: String = ""
@State private var kind: Account.Kind = .checking
func create() {
stateController.addAccount(named: name, withKind: kind)
dismiss()
}
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
}
Here we nd a rst example of mapping the user action into the app’s
business logic. The NewAccountView structure collects the data it
receives from its Content view into two @State properties. Then,
when the user taps on the Create button, it creates a new account by
calling the addAccount(named:withKind:) method of the State-
Controller.
33
fi
fi
fi
fi
func send() {
let amount = (Int(self.amount) ?? 0) * 100
stateController.addTransaction(withAmount: amount, beneficiary: beneficiary,
to: account)
dismiss()
}
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
}
We nally covered the whole app. With the root layer, we provided all
its navigation ow and connected all screens to the central state.
34
fi
fl
fi
fi
fi
I hope you enjoyed this guide and it helped you improve your under-
standing of iOS architecture in SwiftUI apps. It took me many hours
of work to put it together, but I am happy to offer it to you free of
charge.
Think about colleagues and friends that could nd this guide useful
and send them this link through email, on forums, or through a private
message, so that they can get it together with all the other material I
only share with my email list:
https://matteomanferdini.com/architecting-swiftui-apps-with-mvc-
and-mvvm/
You can also use one of these links to share the guide on your favorite
social network.
35
fi
fi