MVVM in iOS with SwiftUI (Detailed Example + Pitfalls)

MVVM in iOS with SwiftUI (Detailed Example + Pitfalls)

Since the introduction of SwiftUI, the MVVM pattern has seen a new renaissance. Many developers believe that this particular pattern fits well with the SwiftUI data flow.

MVVM certainly has some good ideas, but it also brings along problems because of the various discording interpretations of the pattern and its rigidity.

In this article, we will see how MVVM fits in iOS apps written in SwiftUI, how to take advantage of its benefits, and how to avoid its problems.


Chapter 1

How MVVM improves the structure of iOS apps

Chapter 2

The model layer is the foundation of an app’s architecture

Chapter 3

Organizing the view layer and simplifying view models

Chapter 4

The networking infrastructure of an MVVM app

Chapter 5

Bringing an app together using view models

Architecting SwiftUI apps with MVC and MVVM

Chapter 1:

How MVVM improves the structure of iOS apps

How MVVM improves the structure of iOS apps

MVVM is an architectural pattern that helps you structure your iOS apps. While it’s a derivation of MVC, it sports some unique ideas that fit well with how SwiftUI apps work.

The structure of the MVVM pattern and the roles of its layers

If I had to define MVVM quickly, I would say that it’s a variation of the MVC pattern.

But let’s proceed in order.

Like MVC, the Model-View-ViewModel pattern, or MVVM in short, is an architectural pattern that guides how you structure the code in your iOS apps. MVVM consists of three layers, from which it takes its name.

Diagram for the MVVM pattern in iOS apps

Each layer has a well-defined role in the app’s structure, helping you respect the separation of concerns design principle.

  • In the model layer, we find the Swift types representing your app’s data and its domain business logic.
  • The view layer is what the user sees on the screen. In iOS, it contains the SwiftUI views for your app, which display information and allow user interaction.
  • Finally, a view model connects a view to the app’s model. A view model contains the current state of a view, links to other parts of an app’s architecture (e.g., data storage or the network), and handles the user’s interaction.

The MVVM pattern is not unique to iOS. In fact, it was invented by Microsoft architects (of all people). It found its way into iOS apps only years after the release of the first iPhone.

Traditionally, Apple has followed the MVC pattern for both macOS and iOS apps. That changed with the introduction of SwiftUI.

MVVM uses binders to connect views and view models

Looking at the MVC pattern diagram, it does not take a genius to see that MVVM is, practically speaking, the same pattern. For that reason, I usually talk about architecture in terms of MVC, although I use ideas from both patterns.

The most crucial difference with MVC is that MVVM connects the view and the view model layers using a binder. This synchronizes the data between the two layers, removing boilerplate code.

That’s a natural part of SwiftUI.

In UIKit, developers used an FRP framework like RxSwift. SwiftUI instead uses, behind the scenes, Combine, Apple’s native reactive framework. But you don’t need to learn Combine to make SwiftUI apps.

So, in SwiftUI, the most significant difference between MVVM and MVC has disappeared. There is only one way to connect objects and views:

  • An object must conform to the ObservableObject protocol.
  • Such an object must expose any property that affects the user interface using the @Published property wrapper.
  • Views connect to observed objects through the @StateObject, @ObservedObject, and @EnvironmentObjects property wrappers.

MVVM vs. MVC: local view models and of global controllers

There is another fundamental difference between MVC and MVVM.

In MVC, the emphasis has always been on making controllers globally shared objects. It was not uncommon to use singletons for that, although dependency injection became, with time, the preferred alternative.

In MVVM, instead, each view gets its separate view model. Objects, then, move from global to local.

In UIKit, both interpretations were possible because there was a fourth, not-so-hidden layer: view controllers. These sat squarely between views and controllers/view models. Since they were required by the framework, it was impossible to get rid of them.

That does not happen in SwiftUI. If you make an iOS app with more than one screen, you soon conclude that both local and global states are needed.

That’s why SwiftUI offers three property wrappers instead of just one. So, again the difference between MVC and MVVM disappears. We can have both controllers (global) and view models (local) in a full app. 

Chapter 2:

The model layer is the foundation of an app’s architecture

The model layer is the foundation of an app’s architecture

Any iOS app needs solid foundations or the whole castle crumbles. These foundations are the model types, which represent an app’s data.

These are not inert values, though. They contain the domain business logic and the code for data transformation.

Creating model types to represent the data of an app

As an example, we will build a small app for Hacker News, a news website for developers like Reddit, known for its (debatable) quality. 

We will use its simple web API to fetch the top 10 news stories from the best stories page.

Mockup for the Hacker News app to illustrate the MVVM pattern in SwiftUI

You can find the complete Xcode project on GitHub.

As it’s often the case, we will start creating the model layer of our app. These are the easiest to implement and the foundation for the whole app’s architecture.

Model types only contain data and the code that manipulates it. They should not know anything about data storage, networking, or how data is presented to the user. In short, they should not know anything about the other layers of the MVVM pattern.

The Hacker News API uses a single item entity to represent all its data. Stories, comments, jobs, etc. are all items. So, creating a corresponding Swift type is straightforward:

JSON decoding goes inside the model layer

Since the Hacker News API returns data in JSON format, our Item struct must conform to the Decodable protocol and provide some coding keys to map the JSON fields to our properties. You can read more about JSON decoding in my Codable article.

This is an example of code that manipulates data. Model types are not just empty containers. Data transformation code goes into model types together with the domain business logic.

Granted, our Item type has no business logic. That often happens for data we fetch from a web API. You can find an example in my free guide on MVC and MVVM in SwiftUI.

While we are at it, we can also create some test data we will use later for our SwiftUI previews. First, we need to grab the information for a story from the API, which we can save in a .json file in our Xcode project.

Then, we decode it inside a TestData structure to make it easy to use in any SwiftUI preview.

Again, refer to my Codable article linked above for more details about JSON and test data.

Chapter 3:

Organizing the view layer and simplifying view models

Organizing the view layer and simplifying view models

SwiftUI makes it simple to structure views, but it also makes it simple to couple their code to other types, making them less reusable. In this chapter, we will see how to keep view types decoupled from the rest of the app’s code.

Views should be independent of model types

We will now jump to the other edge of the MVVM pattern and create our views.

I am skipping the view model layer because it’s the most complicated. Moreover, it’s hard to write a view model’s code when we don’t have its related view.

We can start with some simple views to represent the position, upvotes, and comments for each row in our News screen.

The Xcode preview for the components of the News screen

I could have used the Label type of SwiftUI instead of creating the Badge view, but the former has a spacing that does not fit my mockup.

What is important here is that our two views are entirely independent of our Item structure. Instead, they use simple Swift types like Int and String. This is an excellent practice to keep your types as loosely coupled as possible so that changes break as little code as possible.

Formatting code should be kept out of views and view models

The view for a single story also looks straightforward. But here we have to pause for a moment.

Our UI requires a lot of data formatting:

  • The numbers for upvotes and comments need decimal separators;
  • We only need to show the domain name for a story, not its full URL;
  • A story doesn’t have a date but shows how much time has passed since its submission.

Formatting is related to the visual representation of data, so its code does not belong to the Item structure.

At the same time, we want to keep views independent and reusable by using simple Swift types. So, where do we put our formatting code?

Some proponents of MVVM think that formatting code should go into view models. But that’s a mistake.

First of all, this would overload view models with responsibilities. Moreover, the formatting code needs to be reusable and not tied to a single view or view model.

While we only have a single screen in our sample app, that rarely happens. A separate screen showing each story’s comments, as it happens on the Hacker News website, would need to format data in the same way.

Luckily, Swift offers a perfect solution: extensions.

Don’t follow the MVVM pattern too strictly

Now, I have a question for you.

To which layer of MVVM does the formatting code above belong?

To be honest, I don’t think that’s a question that makes much sense. This shows you the dangers of adhering too strictly to a particular design pattern.

It looks like the code above belongs to the model layer since we are extending data types like Int, URL, and Date. At the same time, our extensions do not affect those types. We own that code, and we only use it in the view layer.

We can even place it inside the same files as our views. If I had to find a definite answer, I would say that our extensions are still part of the view layer.

Which brings me to the next point.

Keeping the view and model layers decoupled using Swift extensions

This could be a possible implementation for the view representing a story.

This view is coupled with our Item structure.

In a simple app, I would consider that to be acceptable. There is no need to complicate your code and keep it as generic as possible when that’s not needed.

In more complex apps, though, you often need to reuse UI components with different data types. Moreover, the Story view above embeds formatting code that might not apply to all data.

In that case, we need to decouple the Story view from the Item model type.

This does not solve our problem, though. It only moves it up the view hierarchy. The view that contains the Story type will need to use the code we just removed.

Here, Swift extensions come to our rescue again.

We can now initialize our view using an Item value, while its internal code uses only simple Swift types.

We can even export our Story view to another app without the Item type. All we need is a new extension using the model types of the destination project.

We can now use this new initializer in two places. The first is inside the SwiftUI preview.

Xcode preview for the row view

The second is in the view for the whole News screen.

Xcode preview for the complete News view

Chapter 4:

The networking infrastructure of an MVVM app

The networking infrastructure of an MVVM app

Networking is another of the weak points of the standard approach to MVVM. Many developers put networking code inside view models. It’s better to keep such code separate to make it extensible and avoid repetition.

Should networking code go into view models?

Our app needs to fetch data through the Hacker News API. Networking code clearly does not go into either the model or the view layers.

This is another long-standing debate about MVVM, where some proponents insist that networking code goes inside a view model?

I beg to differ.

If you look at it from the point of view of reusing code, that’s a mistake. While a view model clearly needs to trigger network requests to fetch data, networking code is filled with boilerplate.

That’s code that is clearly going to be repeated in every view model that performs network requests.

Here, I think the culprit is the use of reactive frameworks like RxSwift and Combine. All that boilerplate code gets lost into the glue code that connects streams of data.

But that’s just replacing some boilerplate with some other boilerplate.

Creating a separate networking layer to keep boilerplate code out of view models

A better approach to iOS networking in Swift is to build a separate infrastructure for API requests.

Our sample app doesn’t need a networking hierarchy as complex as the one in that article. Still, we can use a generic network request class, which simplifies our view model code and is easy to extend.

To fetch the best stories from the API, we need to perform two types of requests.

  • Usually, REST APIs return arrays of JSON objects with the full data. The best stories endpoint, instead, only returns an array of IDs.
  • After getting those IDs, we need to call the story endpoint several times to fetch each story’s data.

So, our API request class needs to handle different types of JSON data.

In short, the perform(with:) method uses a URLSession to fetch data and then decodes it with a JSONDecoder. Since it uses a Swift generic, we can use this method for any Decodable data.

Again, refer to the article about REST APIs I linked above for a full explanation.

Chapter 5:

Bringing an app together using view models

Bringing an app together using view models

The core piece in the MVVM pattern is the view model layer. This brings together all the other parts of an app’s architecture, connecting views to data and the networking infrastructure.

A view model contains the app’s business logic that drives a single app screen

We can finally focus on the core layer of MVVM: the view model layer.

A view model is limited only to the logic that drives a specific screen in an app. In our example, we only need one for the NewsView type, representing the whole News screen.

Our view model needs to perform all the network requests to retrieve the top 10 best stories from Hacker News.

First of all, the NewsViewModel class conforms to the ObservableObject protocol and publishes its stories property using the @Published property wrapper. This is the “binder” part of MVVM we discussed at the beginning of the article.

While the NewsViewModel class contains the logic to retrieve the stories, it does not know anything about the NewsView. This leaves the view model decoupled from its view, making it easier to write unit tests.

I divided the fetching of data into two methods to avoid callback hell. This is the simplest way to achieve that without using a reactive framework like Combine.

The fetchTopStories() method fetches the IDs of the best stories. Then, it calls the fetchStory(withID:completion:) method for each of these. This, in turn, fetches the details of each story.

All these requests happen in parallel, and their callbacks might occur in any order. Appending the stories to the stories array might then change their order.

To avoid complex synchronization code, I first populate the stories array with 10 nil values and replace them with the corresponding story using indexes.

Creating view models in SwiftUI views and triggering events

All we have left is connecting our view to its view model.

The NewsView type creates the NewsViewModel instance using the @StateObject property wrapper. Then, it triggers the network requests by calling the fetchTopStories() method in its .onAppear modifier.

The @ObservedObject wrapper would be a mistake. The view would recreate its view model every at every refresh of the view hierarchy, with the effect of losing the callbacks of all the network requests in progress.

We could also use the @EnvironmentObject wrapper in this specific case, but that’s not a common practice. View models often need to be initialized with particular values, which is only possible in a view’s initializer. 

The @EnvironmentObject is more appropriate for shared instances of global objects. The ones that are usually called controllers in the MVC pattern.

To complete our app, we only need to add a navigation view to our app’s entry point.

The running Hacker News app


The MVVM pattern provides a useful architectural idea: the views in an iOS app often need dedicated objects. These are the view models of MVVM.

In this article, we have seen one of the most common uses for view models: performing a sequence of network requests to fetch data for a single view.

We also saw how to solve many of the problems of sticking to the pattern definition too strictly. Making formatting and networking code reusable simplifies all the view models in an app.

There is only so much this article can cover about an app’s full architecture. As I mentioned, complex apps often need global objects.

The MVVM pattern alone is too limited. Trying to shoehorn all your code into view models is only going to create problems.

There are also some problems that a strict approach to MVVM does not address. For example, why does the NavigationView in the example above go into the main app structure? And what happens in apps with tricky navigation?

Moreover, adding objects to views breaks Xcode previews. After adding the NewsViewModel instance to the NewsView, you will notice that its preview becomes empty.

To solve all these problems, we need an extra layer, which I call the root layer. To know more about it, get my free guide below.

Architecting SwiftUI apps with MVC and MVVM

It’s easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.