MVVM in SwiftUI for a Better Architecture [with Example]

Since the introduction of SwiftUI, the MVVM pattern has experienced a renaissance. Many developers believe this particular pattern aligns well with the SwiftUI data flow.

MVVM  incorporates good ideas but also introduces problems due to varying interpretations of the pattern and its perceived rigidity.

In this article, we’ll explore how MVVM fits into SwiftUI, how to leverage its advantages, and how to navigate its challenges.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Table of contents

Chapter 1

What is MVVM?

MVVM is an architectural pattern that assists in structuring the code of a SwiftUI app by dividing it into three distinct roles.

  • The model represents the app’s data and its business logic.
  • The view displays information to the user and enables interaction.
  • The view model acts as a bridge between the view and model layers. It contains a view’s state and handles most of its display and interaction logic.

A crucial component of MVVM is the binder, which synchronizes the view and view model layers, eliminating related boilerplate code.

The MVVM pattern is not exclusive to SwiftUI or iOS. It was initially developed by Microsoft architects and later integrated into iOS years after the initial SDK release.


In this chapter:

How MVVM works in SwiftUI

Apple doesn’t explicitly endorse any architectural pattern over another. However, SwiftUI is particularly fitted to the MVVM architecture.

  • It offers numerous data-independent views that seamlessly align with the view layer of the MVVM pattern.
  • It provides mechanisms to bind views to data and automatically update the user interface when the underlying data changes.

In SwiftUI, the model layer consists of Swift types containing the business logic. Typically, these are Swift value types (structures and enumerations), but they can also be objects when utilizing storage solutions like SwiftData or Core Data.

On the other hand, a SwiftUI view model is implemented as an @Observable) class, held by a view within a @State property, and connected through the @Binding property wrapper or action closures to SwiftUI views that allow data input and user interaction.

SwiftUI view models before the Observation framework

In older SwiftUI apps created before the release of the Observation framework, view models were classes conforming to the ObservableObject protocol. These classes needed to expose their properties through the @Published property wrapper and were stored within @StateObject properties.

If your app doesn’t require support for iOS versions before 17, it’s recommended to migrate your codebase to use the Observation framework.

MVVM vs. MVC: Local view models and global controllers

When comparing the MVC pattern diagram, it’s apparent, even to non-experts in graph theory, that MVVM and MVC are nearly identical.

The fundamental disparity between MVC and MVVM in SwiftUI lies in the emphasis on controllers, which are objects shared across several views, versus view models, which are local objects controlling the behavior of a single view—usually representing a single screen in an iOS app.

However, in MVVM, the necessity for shared objects persists. These are often implemented as singletons, subsequently accessed by individual view models.

I consider singletons an anti-pattern, so I combine the MVVM and MVC patterns in my apps, modeling the view logic through view models and sharing controllers via dependency injection.

Why you should use MVVM in your SwiftUI apps

The clearly defined roles in the SwiftUI MVVM architecture enable adherence to the separation of concerns design principle, which is crucial for maintaining well-organized and easily understandable/testable code.

Unfortunately, there’s a misconception among inexperienced developers that MVVM is an obsolete pattern and is no longer necessary in SwiftUI.

However, architectural patterns like MVVM are unavoidable. SwiftUI manages the view layer, but without architectural patterns, code accumulates within views, creating massive monolithic types that are challenging to maintain and test.

The fact that SwiftUI handles view updates automatically doesn’t justify abandoning software development best practices that have existed for decades across various platforms.

Chapter 2

Implementing the MVVM Pattern in a SwiftUI App

To see an example of MVVM in SwiftUI, we will build a small app for Hacker News, a news website for developers akin to Reddit, known for its (debatable) quality. We will use its web API to fetch the top 10 news stories from the best stories page.

You can find the complete Xcode project on GitHub.


In this chapter:

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Creating model types to represent the app’s data and business logic

Let’s initiate the model layer of our app. Model types only contain data and the code that manipulates it. They should remain agnostic of data storage, networking, or user interface presentation.

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

struct Item: Identifiable {
	let id: Int
	let commentCount: Int
	let score: Int
	let author: String
	let title: String
	let date: Date
	let url: URL
}

extension Item: Decodable {
	enum CodingKeys: String, CodingKey {
		case id, score, title, url
		case commentCount = "descendants"
		case date = "time"
		case author = "by"
	}
}

As the Hacker News API returns data in JSON format, our Item struct conforms to the Decodable protocol and provides coding keys to map the JSON fields to our properties. Further details about JSON decoding are available in my Codable article.

Model types should not merely act as empty data containers. As an instance of business logic, data transformation finds its place within the model layer of MVVM.

Implementing views decoupled from model types

Views constitute the second layer we’ll explore. Let’s commence with a view representing a single entry in the top 10 most upvoted posts.

struct Entry: View {
	let title: String
	let footnote: String
	let score: Int
	let commentCount: Int

	var body: some View {
		VStack(alignment: .leading, spacing: 8.0) {
			Text(title)
				.font(.headline)
			Text(footnote)
				.font(.footnote)
				.foregroundColor(.secondary)
			ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {
				Label(score.formatted(), systemImage: "arrowtriangle.up.circle")
					.foregroundStyle(.blue)
				Label(commentCount.formatted(), systemImage: "ellipses.bubble")
					.foregroundStyle(.orange)
					.padding(.leading, 96.0)
			}
			.font(.footnote)
			.labelStyle(.titleAndIcon)
		}
	}
}

#Preview {
	Entry(
		title: "If buying isn't owning, piracy isn't stealing",
		footnote: "pluralistic.net - 3 days ago - by jay_kyburz",
		score: 1535,
		commentCount: 773
	)
}

The crucial aspect is that our view remains entirely independent of our Item structure. Instead, it employs simple Swift types like Int and String.

Additionally, the Entry view does not encompass information such as:

  • the domain name of the website;
  • the date of the story;
  • the author.

It merely displays a footnote text, requiring only that information. This practice aids in maintaining types as loosely coupled as possible, minimizing the impact of changes on existing code.

Writing a view model that encapsulates the app’s logic for a single screen

The full view necessitates displaying a list of stories fetched from the website. This is where most developers typically place the required state and networking code. However, according to the MVVM pattern, that code belongs in a view model.

To retrieve the best stories from the API, we need to execute two types of requests:

@Observable
class ViewModel {
	var stories: [Item] = []

	func fetchTopStories() async throws {
		let url = URL(string: "https://hacker-news.firebaseio.com/v0/beststories.json")!
		let (data, _) = try await URLSession.shared.data(from: url)
		let ids = try JSONDecoder().decode([Int].self, from: data)
		stories = try await withThrowingTaskGroup(of: Item.self) { group in
			for id in ids.prefix(10) {
				group.addTask {
					return try await self.fetchStory(withID: id)
				}
			}
			var stories: [Item] = []
			for try await item in group {
				stories.append(item)
			}
			return stories
		}
	}

	private func fetchStory(withID id: Int) async throws -> Item {
		let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!
		let (data, _) = try await URLSession.shared.data(from: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		return try decoder.decode(Item.self, from: data)
	}
}

The ViewModel class is @Observable, implying that any update to its stories property will trigger an update in views utilizing it to populate their user interface. Placing this code outside of SwiftUI views simplifies testing.

The fetchTopStories() and fetchStory(withID:) methods employ Swift’s async await and URLSession to download data from the Hacker News API. Furthermore, the fetchTopStories() method utilizes a task group to parallelly retrieve the data for each story, creating distinct subtasks for each fetchStory(withID:) call.

I’ve placed all the networking code within the view model in this simple example. However, for a more sophisticated SwiftUI app, a preferable approach to reusing code is constructing a separate Swift networking infrastructure for REST API calls.

Adding a view models to a SwiftUI view and triggering events

The final step to implement MVVM in a SwiftUI app is connecting the view model to a view.

struct NewsView: View {
	@State private var model = ViewModel()

	var body: some View {
		List(model.stories) { story in
			Entry(story: story)
		}
		.listStyle(.plain)
		.navigationTitle("News")
		.task {
			try? await model.fetchTopStories()
		}
	}
}

extension Entry {
	init(story: Item) {
		title = story.title
		score = story.score
		commentCount = story.commentCount
		footnote = (story.url.host() ?? "")
		+ " - \(story.date.formatted(.relative(presentation: .numeric)))"
		+ " - by \(story.author)"
	}
}

#Preview {
	NavigationStack {
		NewsView()
	}
}

The NewsView type initializes the ViewModel instance within a @State property. It then initiates network requests by invoking the fetchTopStories() method within its .task modifier and exhibits the contents of the stories property of the view model using a List.

The Entry extension enables convenient initialization of the view using an Item value within the NewsView, despite the Entry view utilizing simple Swift value types.

To display the navigation bar when running the app in the iOS simulator, remember to include a NavigationStack in the app structure.

struct HackerNewsApp: App {
	var body: some Scene {
		WindowGroup {
			NavigationStack {
				NewsView()
			}
		}
	}
}

You might notice a distinction between the Entry and NewsView types. While the former remains wholly decoupled from other app layers, the latter is linked to the ViewModel class and, consequently, to the Item structure.

The NewsView qualifies as a root view. Although it resides in the view layer, it shoulders more responsibilities than a pure view, which solely displays data and facilitates interaction. Further insights into this concept are available in my free guide on MVC and MVVM in SwiftUI.

Chapter 3

Alternative architectural patterns and similar ideas

MVVM isn’t the sole architectural pattern available in SwiftUI. As previously mentioned, MVC is the other obvious choice and, in my opinion, is essential for any substantial app.

I’ll begin this section with a disclaimer: it will reflect my strong opinions and critical perspectives on these alternative patterns.

However, sometimes, valuable ideas emerge that can be integrated into your app’s architecture. So, take everything I discuss here with a grain of salt.


In this chapter:

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

The Model-View pattern and the Elm architecture

Let’s address the primary contender: the MV pattern (Model-View). There’s extensive debate on Apple forums asserting that MVVM isn’t necessary for SwiftUI and that an app should primarily comprise views and the model.

This concept is occasionally likened to the Elm architecture and often stems from a slide in Apple’s initial SwiftUI presentation.

I lack sufficient knowledge of web development and Elm to comment definitively on this. However, drawing from two decades of developing macOS and iOS apps, I know that shared state within views is unavoidable.

I’m not attributing this to Apple. Their slide merely illustrates how SwiftUI interacts with an app’s state. It doesn’t advocate for any specific architecture or imply that such a state should exist within a single architectural layer.

Delving into the MV pattern reveals objects shared across views through singletons or as SwiftUI environment objects.

This reiterates the MVC pattern. You might label these objects as handlers, managers, stores, services, or any other fancy name, but fundamentally, they remain controllers.

However, foregoing a well-defined design pattern leads to these objects lacking distinct roles. Consequently, this results in a messy base and model types that encompass responsibilities belonging to other layers.

The Clean Swift architecture and the VIP and VIPER patterns

A family of patterns, including VIP (View-Interactor-Presenter) and VIPER (View-Interactor-Presenter-Entity-Router), stems from what’s commonly known as Uncle Bob’s Clean Architecture.

Much could be said about these patterns, but I perceive them as a rehash of MVC with shuffled roles.

In my opinion, one significant flaw is the proliferation of protocols and generics. While both are necessary in complex apps, every abstraction incurs a cognitive cost. Such abstractions should only be used when truly beneficial, not merely because a pattern prescribes them.

The influences of Redux and the Swift composable architecture

Among the options discussed here, the Swift composable architecture seems to be gaining recent popularity, especially with support from a dedicated open-source library.

I believe Redux-like architectural patterns, such as the Swift composable architecture, represent the least favorable choice.

The fundamental idea is to impose functional programming paradigms onto a language and a framework that are not inherently functional but heavily reliant on imperative code and state.

The primary issue with this approach is the concentration of the entire application state within a single monolithic structure, resulting in tightly coupled code. Moreover, any change to this structure can trigger view updates across the entire view hierarchy, even in views unrelated to the updated state components.

Another problem arises from reducers, introducing substantial boilerplate code typically implemented using numerous Swift switches, blatantly violating the Open-closed principle.

Conclusions

The MVVM pattern introduces a crucial architectural idea: the necessity of dedicated objects for views in an iOS app, namely the view models of MVVM.

This article has limitations in covering an app’s entire architecture. As previously mentioned, complex apps often necessitate global objects.

Relying solely on the MVVM pattern proves overly restrictive. Attempting to force all code into view models will likely lead to complications.

Additionally, a strict adherence to MVVM fails to address specific problems. For instance, why does the NavigationStack in the above example fit into the main app structure? What happens in apps with intricate navigation systems?

Furthermore, adding objects to views can easily disrupt Xcode previews.

An extra layer—the root layer—is required to resolve these issues. For more information, access 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.

GET THE FREE BOOK NOW

32 thoughts on “MVVM in SwiftUI for a Better Architecture [with Example]”

  1. Thank You. Good article.
    Can you give advice? How code will look if flight will change at another place and FlightCardView should update data? Who will observed changes?
    Maybe do you have a article for this question? :)

    Reply
  2. Thank you for the article.

    One thing I’m still confused about in your example, is FlightCardView a model or a view controller? What is confusing me is the fact that the view outlets are contained within the FlightCardView class. I can see them all as a struct within FlightCardView but FlightCarView also appears to be changing the view when it places string data in the text properties outlets. In your diagram you show the view reading data from the view mall but it seems like the view model is pushing data into the view. Could you clarify this?

    Reply
  3. Thanks for this amazing article, Matteo!
    I’m curious about one thing: in the example project, your View owns the ViewModel. I thought the MVVM architecture said your ViewController should owns it instead.
    Greetings.

    Reply
  4. What are your thoughts on UIKit independence in ViewModels? If a view has a UIImageView, should the ViewModel have a UIImage property? a Data property? Probably not a string url, because then networking code has slipped into your view / view model layer.

    Reply
  5. Thanks, this was really helpful. I found myself making several of the errors mentioned here. I have a question: under this pragmatic approach, how would you update views after a model change? In this code example, let’s say you receive a push notification about flight departure time change, how would you go from updating the model up to the view that is displaying it?

    Reply
  6. This approach works well for simple views like labels, but how do you apply this methodology when you have more complex/dynamic views, such as UITextFields? You need to send along a delegate. I suppose you could have the view controller be the delegate and pass a reference through via the view model, but that starts to get messy, especially if there are multiple text fields that you need to distinguish between.

    Reply
    • One of the points of this approach is indeed to avoid having the view model as a delegate, or respond to any other kind of message or notification.

      What you propose depends on what you are trying to accomplish.

      For example, if you need to respond to delegate messages from some control just to make layout adjustments in the UI, you should create a custom view class which then acts as a delegate.

      UI is the domain of views. If there is no other consequence, there is no point in propagating it to the view controller layer.

      In other cases though it might be necessary to have the view controller as a delegate. If that starts to become messy, it means you probably have to create a seaprate model controller. A very comon case is a data source for a table view. A good practice is to always create a separate model controller for that.

      I show that in this article: http://matteomanferdini.com/uitableview/

      Reply
  7. Very nice article, thank you. Now suppose I have a UILabel of which the font color changes with the value it is showing. Maybe indicating the flight delay time: for instance, no delay: green, < 30 min: orange, > 30 min: red. Where would the logic to determine the color go, in the view or view-model?

    Reply
    • Good question.

      In general, keeping logic in a structure keeps it more testable. It’s easier to write a unit test for a structure, than it is to write it for a view.

      So I would put this logic in the view model. It also makes sense form the point of view of formatting data: you are turning a time interval into a color.

      Finally, the delay logic is app’s logic. The view does not care about how much delay is in any category. It just cares about the category.

      Reply
  8. Very good!!! Thank you for sharing. A doubt, in case we have a registration view (for insertion of information), how the view model would behave? The view model would be responsible for the transformation the view to the model? We would have:

    private extension FlightInfoViewController {
    func convertViewModelToModel() -> Flight {
    viewModel.airportSymbols = airportSymbolsLabel.text
    viewModel.departureDay = departureDayLabel.text
    viewModel.departureAirport = departureAirportLabel.text


    }
    }

    And this model would be used in the model controller to persist the data. Is this approach correct?

    Reply
    • Yes, you would use a view model to make the reverse transformation as well.

      In fact, you should use the same view model. Chances are you can even reuse some of the code.

      Just add another initializer to the ViewModel struct, which takes the formatted values coming from the view and transforms them back into data you can use in the model controllers of your app.

      You send this back to the view controller using delegation in your view, like UITextFieldDelegate does. Then the view controller propagates it to model controllers.

      Reply
  9. Who is responsible for @IBActions and who should act as a delegate for the subviews in the custom view? For instance, I have a form with a submit button which should only be enabled when all input fields are valid. In the MVC approach, the VC as a UITextField delegate would validate the input, enable/disable the button and act when the button is tapped. But who’s responsibility is this in your MVVM approach? Since the VM “does not receive callbacks and notifications” and “does not update views”, that leaves the VC and the custom view?

    Reply
  10. Hi, thank you for a grate article! I really love your perception – MVVM pattern is an extension of the MVC pattern. Also, I have few questions. According to your comment – ‘Validation happens in the view controller and attached model controllers, but only for the data itself.’ – you means that it belongs to view controller or to model controller. For example, I’m talking about validation for input text field – password.
    As ViewModel responsible to format the presented ‘output’ – data for View, if so it sounds to me logical that ViewModel also should to perform a validation on incoming ‘input’ – data of the Views.
    1. What do you think about it. Where is the right place to put a validations, can you please explain it in more details where to put it and why it should be there.
    2. On the other hand, in case of Login screen when I have a View with two empty input fields email and password and I implemented a validations in the Controller, it seems to me that I don’t need a ViewModel layer at all. Am I right?

    P.S. I was looking for ultimate solution for validations and I found another inspiring article ‘Mixins and Traits in Swift 2.0 ‘ article by @mhollemans, that proposes to implement validations using protocol extensions. https://goo.gl/kAK4av

    Reply
    • It depends what you mean by validation.

      If you mean transforming formatted data on the screen to the model of your app, yes, that would go in the model.

      There is a similar question in another comment, check the gist I made in reply: http://matteomanferdini.com/mvvm-pattern-ios-swift/#comment-3797462443

      If validation means includes business logic, instead, that would not go in a view model, or you would spread it around your app and would have to duplicate it in other places.

      In general, my approach to view models is to remove from view controller knowledge about the UI. Keep in ming that you don’t need always a view model. If data can be displayed as it is, then you would have no view model at all, as you said.

      I personally don’t like design patterns that force you to implement types that are not needed.

      Regrading the article you linked and the bigger picture: validation should *happen* in the view controller, but that does not mean that the view controller should contain that code.

      In fact, I would also put in separate objects like the validators in the article you linked. The view controller should only dispatch execution between views, view models, and other objects.

      Reply
  11. Hi, big thanks for really great article. I have one question: If our views always owns it’s View Models, maybe it will be good to implement some protocol for this first like that:
    protocol ModellableView {
    associatedtype VModel
    var model: VModel? {get set}
    }
    and implement our custom views like that:
    class UserView: UIView, ModellableView {
    typealias VModel = ViewModel
    ….
    var model: VModel? {
    didSet {
    ……
    }
    }
    }
    What benefits we’ll get from that implementation or this is not good aproach?

    Reply
    • That would not produce any benefit. In the end, the view model of a view is a specific type and there is no real reason to abstract it.

      Your protocol only expresses that the view has a viewModel property, but that the compiler knows anyway, so you just get some more code.

      This king of approach (or in genera, abstraction) can be useful when you need to share code between types. But in this case, you added a few lines without reducing any other code in the app.

      Reply
  12. Hi Matteo, thank you so much for the great articles you share. I have a question about how would you hold some external functionality for models, for example each model have data representing an audio player, inside the model I would store the name and progress for example but where would you store the player instance itself so you can call functions on it? Is it something the model should be aware of? Or maybe a model controller? If you have reference to material deals with this question it would be wonderful. Thanks so much!

    Reply
    • This is hard to anwer, since I would have to see the UI of the app and its business logic.

      When you talk about audio player, it’s not clear to me why you would have more than one.

      If your all plays audio files, you probably have a list of files (songs, podcast episodes, etc). For each one you might keep track of the listening position, to resume playback at the same place. That’s part of the model of the app, so these would be all structs. The “player” is the screen with controls to start and stop.

      You might instad be writing a multitrack app, like a song mixing software or the like, where you see on the screen multiple tracks which can all play at the same time.

      This would be indeed more complicated. The view controller would hold a list of reference to audio playing objects, which, in turn, play different tracks. You could consider these to be model controllers, if we want to use strict definitions.

      But each player on the screen can be seen as a single view (probably a cell in a table view), with a view model containing all the data it needs. You would configure each view model using both the state of each single player and the properties of each audio files.

      Reply
  13. How would the MVVM pattern work with Core Data in SwiftUI? For example I want my VM to fetch data from the database or from the API if there is no data persisted. I know that with @FetchRequest you can fetch data directly from the view, how would that work in the MVVM pattern?

    Reply
    • That depends a bit on the structure of your app, but in general, you should have a global controller that handles Core Data that you share as an environment object, so you don’t replicate that code. Then, single view models can interact with this controller.

      Reply
  14. Thank you for a great post! It would be great if you could include information about how to use CoreData with SwiftUI in this architecture. Should CoreData be abstracted out into its own layer or should you take advantage of the options to integrate it directly into the view with FetchRequest and FetchedResults.

    Reply
    • Core Data really deserves an article on its own.

      In short, yes, I would use the same architecture. That should not depend on the specific implementation details.

      The unfortunate compromise of Core Data is that everything is an object. So you need to be more careful to avoid Core Data code leaking into all parts of your apps. But, in the end, SwiftUI bindings are references too, so it makes not much difference.

      Regarding @FetchedRequest, it is convenient but makes me also raise an eyebrow. I rarely go against the platform, but I haven’t carefully considered this yet. On the surface, though, I can say that using it in my root views approach is definitely going to limit the eventual architectural problems that might create.

      Reply
  15. Great Example; however, What if you want to EDIT data in your detail view? How do you pass the elements of the ForEach into the detail view with a @Binding so that they can be edited? I would like to see an example of how to do MVVM with an editable detail view.

    Reply

Leave a Comment