MVVM and SwiftUI
SwiftUI is not MVVM, but they go really well together. What’s the Model-View-ViewModel (MVVM) architectural pattern and how do you use it? In this tutorial, we’ll discuss how you can use MVVM to better organize your app’s code.
Here’s what we’ll get into:
- How to see iOS development with SwiftUI through the lense of MVC and MVVM
- All the roles: models, views, controllers, view controllers, ViewModels
- Why SwiftUI and MVVM go so well together
- Tools for MVVM: bindings,
@ObservedObject
and Combine - MVVM’s concepts to make your code easier to read, maintain and extend
- Model-View-Controller vs. Model-View-ViewModell; pros and cons
Ready? Let’s go.
- Preface: MVVM and SwiftUI
- What’s MVVM?
- MVC vs. MVVM
- Working with Models
- Working with Views
- MVVM after MVC
- The ViewModel
- MVVM and SwiftUI
- MVVM and SwiftUI in Practice
- Decoupling the View and Model
- Coding The BookList View
- Creating The ViewModel
- Combining the View and ViewModel
- Wrapping Up
- Further Reading
Preface: MVVM and SwiftUI
MVVM stands for Model-View-ViewModel and it’s a software architectural pattern. It’s an approach you can use to organize your code better, and to make it easier to read, maintain and extend.
To get straight to the point: MVVM is special on iOS, especially combined with SwiftUI. MVVM is as much an answer to good ol’ Model-View-Controller (MVC) as it is trying to get away from it; MVVM has a cult-like following on iOS; and even though “MVVM != SwiftUI”, the desire to make everything MVVM is so prevalent in iOS development these days.
In this tutorial, we’re going to discuss what MVVM is, but more importantly, what MVVM’s role is vis-a-vis SwiftUI. Can we use the best of both worlds to create a design pattern that gets out of your way?
Let’s dive in!
What’s MVVM?
Model-View-ViewModel (MVVM) is a software architectural pattern or design pattern, which is a way to organize your code better. It’s organized around 3 roles: Models, Views and ViewModels. A special place is reserved for bindings.
As a software developer, you want to organize your code better, so it’s easier to read/comprehend, easier to fix bugs and easier to add/remove features. A common foundation makes collaboration with other developers easier, too.
MVC vs. MVVM
Just as MVC – which can be regarded as MVVM’s cousin – MVVM separates components in your code into roles.
-
Model: A model is a representation of data. They are the bits and bytes your app moves around, from tweets to
User
objects to to-do list tasks. - View: A view is responsible for displaying data to the user of your app, and handling interaction. It’s the UI of your app, or part of it.
- ViewModel: A ViewModel represents the state of model data. It facilitates communication between views and models, through bindings, or other events and actions.
- Bindings: A binding isn’t exactly a role, but it’s a central part of MVVM. Think of a binding as a connection between a piece of data and the view that displays or manipulates it.
Here’s a quick comparison between MVC and MVVM:
MVVM | MVC | |
---|---|---|
Model | Representation of data model or domain model | Responsible for data, logic and ‘business rules’ of app |
View | Displaying the UI on screen, as well as handle user interaction | (idem) |
Controller | n/a | Ties the models and views together, handling actions for both |
ViewController | n/a | Everything that’s not a View or a Model |
ViewModel | State; sits between views and models, facilicates communication between them | n/a |
Based on the above table, 2 things become clear:
- Software architectural patterns are often overly complicated
- Both MVVM and MVC focus mostly on what goes on between a View and a Model
Before we discuss the ViewModel, View Controller and bindings, let’s take a look at models and views first.
Working with Models
In modern software development, the data models you work with are often crystal clear and well-defined. You’ve got a User
, Tweet
or Invoice
object, and you know exactly what kind of properties it has.
Like this:
struct User {
var id = UUID()
var name: String
var email: String
}
Thanks to modern components like Codable, you can often map a Swift struct directly to a JSON API or a compatible data source. With Realm, objects returned from the database are native Swift types like the above struct. You can often match domain models directly and intuitively to Swift types.
Note: A problem with models is when you’re not sure about how to structure your data, or when the domain models change further on in a project. It’s worth it to learn more about relationships, Object-Relational Mapping, SQL queries and normalization techniques.
Working with Views
What about the view? Its role is clear: display a (part of a) User Interface, and handle user interaction. You interact with the view and it shows you a visual representation of the data you’re manipulating.
Historically, iOS development has always focused on Model-View-Controller (MVC). In practice that meant you’d often end up with a bunch of models and view controllers with lots and lots of code.
The view controller stands in the way of clearly separating view and model logic, because views and models communicate with each other through the view controller; there’s just no other route to choose from.
This leads to what’s affectionately called “Massive View Controller” – you just put all your code in the view controller, with horrible spaghetti code as a result.
Note: A problem with views is often modularity and/or decoupling. How do you break up or trim down views, so they don’t take on too many responsibilities? And how do you keep views separate from the logic in your code, so you can reuse them?
MVVM after MVC
What do you do now? You realize that most communication between the model and the view is the retrieval and manipulation of model data (in/to the view) and changes sent to the model (from the view). The view plays a central role in this; it both pulls data from the model and pushes it back.
Because this communication is so uniform, MVVM has a special component called a binding. It essentially connects a model property to a (sub)view. You ideally have a 2-way binding that reads from the property, and writes back to it.
An example: You bind a Bool
property to a on-off toggle switch; the state of the toggle is now written back to the property whenever you switch it, and read from the property when the toggle is shown on screen.
Another similarity between MVC and MVVM stands out: model manipulation, mostly around formatting. Consider that you’ve got a User
table in a database, with the user’s address neatly organized in 4 or 5 columns. In your app, you’ll only want to show this data as a formatted address.
With a view controller, you’d be compelled to format the address every time you read from the model. With MVVM’s emphasis on “not Controller”, you’re encouraged to put model-transforming code elsewhere.
Like this:
struct User {
var id = UUID()
var name: String = ""
var email: String = ""
var address_1: String = ""
var address_2: String = ""
var address_postcode: String = ""
var address_city: String = ""
var address_country: String = ""
var address: String {
[address_1, address_2, address_postcode, address_city, address_country]
.filter { !$0.isEmpty }
.joined(separator: "\n")
}
}
The above User
model exposes a computed property address
, which is based on the data in its individual address properties. You could even make these properties private to indicate that the address
property should only be read from. It’s also a common practice to put formatting code into an extension.
A common discussion point is whether ‘x’ belongs in the model, view, ViewModel or elsewhere. You can put formatting code in the view if it’s specific for that view. After all, it’s formatting. You can put networking code in the ViewModel, but it may be smarter to abstract it in some controller or manager component. Similarly, you can put queries in your models, but it may be smarter to put them in an API that (only) returns models.
The ViewModel
Now that we’ve put some space between us and the View Controller, and discussed models and views, let’s address the elephant in the room: What the heck goes between the view and the model!?
It’s it a bird? Is it a plane? It’s a ViewModel! *reluctant clapping*
In SwiftUI with MVVM, the ViewModel sits between a View and a Model. It’s the layer that mediates between them, and it has 2 distinct tasks:
- Provide bindings to the view, from the model, for its properties
- Provide data from the model, so it can be displayed in the view
It’s easiest to picture a View Model that sits on top of the View; it belongs to the View. The ViewModel contains references to one or more models. (Hence, ViewModel and not ModelView.)
A view should stay loosely-coupled from the model(s) it uses, but that’s not always possible. When the view owns the ViewModel, and the ViewModel owns the model, the view doesn’t have to own the model.
However, because you often inject models directly into a view, and bind to its properties, it’s often easiest to provide the model object to the view. That doesn’t mean they’re tightly-coupled, because you can still use other approaches (protocols, generics, etc.) to keep your views modular.
A helpful by-effect of SwiftUI’s focus on single source of truth is a clear separation of concerns. The source of truth remains with the ViewModel, even if you copy a model into a view.
We’ll return to the ViewModel shortly.
Note: When you’re building your app, not every component should be either a view, a model or a ViewModel. You can use other supporting design patterns, such as a Helper, Controller, Manager, Façade, Factory, etcetera. The goal is “easy to read, maintain and extend”. It’s often smart to not stick to an architectural pattern for its own sake; the design pattern is here to serve you, not the other way around. (This is also the gray area where you learn the most about building software! Aim to make lots of mistakes here.)
MVVM and SwiftUI
A common misconception about SwiftUI is that “SwiftUI is MVVM”, as if both are inextricably intertwined. This assumption is based on the fact that SwiftUI uses bindings, and some of its property wrappers look a lot like ViewModels.
It’s true that MVVM and SwiftUI are a great match; you’ve got the views, models and bindings – why not throw in a ViewModel as well? SwiftUI doesn’t require you to create a ViewModel though, but you’re probably using something that looks like a ViewModel already.
SwiftUI belongs to the view layer. It provides a declarative API to build views, as well as a (semi-)comprehensive suite of UI components. On top of that, SwiftUI uses property wrappers like @State, @Binding and @ObservedObject to make a view dependant on the state (i.e., value) of a property. Whenever the data changes, the view updates automatically.
That looks a lot like a binding already, without having provided any explicit code to facilitate the communication. A ViewModel provides a binding to the properties of its model data, and a SwiftUI view automatically updates when that data changes. Neat!
SwiftUI has another ace up its sleeve: Combine. Even though Combine is a separate framework, it works incredibly well with SwiftUI. The blurry lines between SwiftUI, MVVM, property wrappers and Combine are a feature, not a bug.
In short, Combine works with publishers and subscribers to set up many-to-many data flow. Think of it as NotificationCenter on steroids. It’s syntax is incredibly concise, and it comes built-in with modifiers for manipulating data. Even though much of Combine’s code is going to be async, it reads as synchronous code similar to how promises do that.
Now that we’ve charted out the playing field for MVVM, let’s write some code!
MVVM and SwiftUI in Practice
With MVVM, you can start building the model or the view first.
- Build the views first if you’re not sure about what kind of data you’ll work with. That’ll give you some ideas for your data models.
- Build the models first if you already know what data you’ll use. That’ll make it easier to build the views, because you can use actual model objects.
In this tutorial we’re creating a simple Books app. Here’s the model we’re using:
struct Book: Identifiable, Codable {
var id: Int
var title: String
var author: String
}
It’s a simple Book
struct with 3 properties that conforms to the Identifiable
(i.e., has an id
property) and the Codable protocols. The JSON data for the books in our apps comes from this URL.
Much of the code you’ll see in this section comes from this tutorial about @ObservedObject
. That tutorial focuses mostly on property wrappers and how they work, whereas the tutorial you’re reading right now focuses on MVVM. When in doubt, read both!
The books in our app will be shown in a List view, which displays rows in one vertical column. Each row consists of a BookRow
view. Like this:
struct BookRow: View
{
var book: Book
var body: some View {
VStack(alignment: .leading) {
Text(book.title)
Text(book.author)
}
}
}
It’s a simple view that shows 2 Text
views in a VStack
. You can also see the book
property of type Book
, that we defined earlier. Inside the Text
views, the properties title
and author
on book
are shown.
We’ll provide Book
objects to the BookRow
view through the ViewModel, which we’ll create later on. For now, it’s worth it to discuss the dependency of the BookRow
view on the Book
model. Both are tightly-coupled; you cannot use one without the other. How can we decouple them?
Decoupling the View and Model
A promising approach is to create a uniform DetailRow
view and a Detailable
protocol. Like this:
protocol Detailable {
var title: String { get }
var subtitle: String { get }
}
We can then make Book
conform to Detailable
by using protocol extensions:
extension Book: Detailable {
var subtitle: String {
return author
}
}
Note how Book
already has a title
property, as required by the protocol. The subtitle
property is added in the extension, which is effectively an alias for author
.
Next, we’re changing the implementation of BookRow
into DetailRow
. Like this:
struct DetailRow: View
{
var item: Detailable
var body: some View {
VStack(alignment: .leading) {
Text(item.title)
Text(item.subtitle)
}
}
}
The DetailRow
is now loosely-coupled with the Book
model, because you can reuse the DetailRow
for any model that conforms to Detailable
. Neat!
Another approach is to deconstruct a model’s properties in the view’s initializer, i.e. duplicating each model property in the view. You can then use the memberwise initializer to set the view’s properties directly. Something like DetailView(title: book.title, subtitle: book.author)
. This prevents you from binding to the model object directly, though.
Coding The BookList View
Now that we’ve got a Book
model and a BookRow
view, let’s put them together in a List view of books.
Like this:
struct BookList: View
{
var body: some View {
NavigationView {
List(books) { book in
let index = ···
NavigationLink(destination: BookEdit(book: binding)) {
BookRow(book: book)
}
}
}
}
}
In the above code you see a simple List
view. It’ll iterate over each object in a yet-to-be-defined books
array. The view also uses NavigationView
and NavigationLink
to set up navigation to a separate BookEdit
view, a UI in which you can edit the properties of a book.
What we need at this point is a component that’ll get Book
model objects, so we can use them in the different views of the Books app. That’s where the ViewModel comes in!
Creating The ViewModel
The ViewModel facilitates communication between the views and models in the Books app. It sits “on top” of views that use Book
model data – this is important to keep in mind.
The ViewModel belongs in the view, but doesn’t necessarily belong to the view. You can reuse the same ViewModel for multiple views, or use one central starting point from which the model data flows into the views.
Take a look at the following code:
class BooksViewModel: ObservableObject
{
let url:URL! = URL(string: "···/books.json")
@Published var books = [Book]()
init() {
fetchBooks()
}
func fetchBooks()
{
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [Book].self, decoder: JSONDecoder())
.replaceError(with: [])
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.assign(to: &$books)
}
}
The above BooksViewModel
uses the ObservableObject
to emit data when its properties annotated with @Published
change. Differently said, when the data in the books
array changes, any view that observes the BooksViewModel
will update itself.
This is the quintessential role of the ViewModel: provide the state of the data models to the view, through bindings (or direct access). Any view can now read from the books
array, and get notified when its state changes.
Want to know how ObservableObject
, @Published
, and the Combine code in fetchBooks()
works? Check out this tutorial: @ObservedObject and Friends in SwiftUI, and this one: Combining Network Requests with Combine and Swift
Combining the View and ViewModel
The individual pieces of the Books app are now complete. All that’s left is putting them together. Check out the updated code for the BookList
view:
struct BookList: View
{
@ObservedObject var viewModel: BooksViewModel
var body: some View {
NavigationView {
List(viewModel.books) { book in
let index = ···
NavigationLink(destination: BookEdit(book: $viewModel.books[index])) {
BookRow(book: book)
}
}
.onAppear {
viewModel.fetchBooks()
}
}
}
}
In the above code, 3 things are important:
- The
BookList
view has a propertyviewModel
of typeBooksViewModel
. This property is wrapped by@ObservedObject
, which will make the view dependant on the state of its@Published
properties. This property wrapper also provides bindings for its properties, among other things. - Inside the
List
, we’re providing aBookEdit
view. This view needs a binding to aBook
object, which we get from the ViewModel using theindex
ofbook
in theviewModel.books
array. This binding lets theBookEdit
view manipulateBook
model data. - In the
onAppear { ··· }
lifecycle action, we’re instructing theviewModel
to fetch book data every time the view appears. This kicks off the HTTP request to the JSON data and its transformation intoBook
objects. You can put this code anywhere; fetching data can be the result of user interaction, for example.
It’s worth noting that the BookList
view only owns a reference to the ViewModel, it hasn’t initialized it. In this example, the BooksViewModel
instance is injected into the BookList
view when that was first created. It’s yet another way to avoid tightly-coupled code, because it leaves the door open for dependency injection, for example.
Wrapping Up
We started this tutorial by discussing how Model-View-ViewModel (MVVM) is affected by long-standing issues with Model-View-Controller (MVC). We also discussed the benefits of separating components of your app in different distinct roles, such as models and views.
The purpose of a software architectural design pattern is to help you organize your code better. MVVM’s goal is to connect models and views with each other, while keeping them loosely-coupled. It does so by providing an approach for bindings, as well as some rules around what a ViewModel should and shouldn’t do.
We’ve also looked at SwiftUI and MVVM, and how they’re not the same, yet easily combined. SwiftUI provides tools to implement MVVM, such as property wrappers. Within the iOS development ecosystem – after MVC – MVVM gets swept up in the excitement around SwiftUI, and it’s easy to assume both SwiftUI and MVVM were always meant to be together.
Reality is different. You’ve seen in the code we’ve written that there are many potential implementations for MVVM, and that there’s no one approach to rule them all. If anything, MVVM is a warning system that helps you think better about the structure of your code – and it gives you ample starting points and tools to do so.
Further Reading
Want to learn more? Check out these resources: