MVVM and SwiftUI

Written by LearnAppMaking on April 10 2021 in App Development, iOS, SwiftUI

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.

  1. Preface: MVVM and SwiftUI
  2. What’s MVVM?
  3. MVC vs. MVVM
  4. Working with Models
  5. Working with Views
  6. MVVM after MVC
  7. The ViewModel
  8. MVVM and SwiftUI
  9. MVVM and SwiftUI in Practice
  10. Decoupling the View and Model
  11. Coding The BookList View
  12. Creating The ViewModel
  13. Combining the View and ViewModel
  14. Wrapping Up
  15. 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:

  1. Software architectural patterns are often overly complicated
  2. 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:

  1. Provide bindings to the view, from the model, for its properties
  2. 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:

  1. The BookList view has a property viewModel of type BooksViewModel. 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.
  2. Inside the List, we’re providing a BookEdit view. This view needs a binding to a Book object, which we get from the ViewModel using the index of book in the viewModel.books array. This binding lets the BookEdit view manipulate Book model data.
  3. In the onAppear { ··· } lifecycle action, we’re instructing the viewModel to fetch book data every time the view appears. This kicks off the HTTP request to the JSON data and its transformation into Book 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:

LearnAppMaking

LearnAppMaking

At https://www.appypie.com/blog/swift, app developers learn how to build and launch awesome iOS apps.