Skip to main content
Swift for iOS Development
CHAPTER 17 Beginner

MVVM Architecture in iOS Apps

Updated: May 16, 2026
7 min read

# CHAPTER 17

MVVM Architecture in iOS Apps

1. Introduction

If you pack all your variables (@State), your network downloading functions, your math calculations, and your UI design into a single ContentView.swift file, you create what developers call a "Massive View Controller." The file becomes 2,000 lines long, impossible to debug, and impossible to test. Professional engineers separate concerns. The visual design should know *nothing* about how the data is downloaded. In this chapter, we will master the industry-standard architecture for modern iOS development: MVVM (Model-View-ViewModel). We will learn how to extract logic into an ObservableObject class and seamlessly bind it to our Views.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Define the roles of the Model, the View, and the ViewModel.
  • Create a class conforming to the ObservableObject protocol.
  • Broadcast data changes using the @Published property wrapper.
  • Instantiate and persist ViewModels using @StateObject.
  • Separate business logic entirely from visual rendering code.

3. The MVVM Trinity

MVVM strictly divides your application into three distinct layers:
  1. 1. Model: The raw data structure (e.g., struct User). It does nothing but hold data.
  1. 2. ViewModel: The brain (a class). It holds the data array, fetches it from the internet, and performs all logic. It prepares the data for the View.
  1. 3. View: The face (the SwiftUI struct). It has zero logic. It simply asks the ViewModel for data and draws it on the screen.

4. Step 1: The Model (The Data Shape)

We define the absolute raw shape of our data.
swift
12345678
import Foundation

// MODELS are almost always simple Structs.
struct User: Identifiable {
    let id = UUID()
    let username: String
    let followers: Int
}

5. Step 2: The ViewModel (The Brain)

We create a class that conforms to ObservableObject. This protocol gives the class a "radio transmitter". We mark any variable that the View needs to care about with @Published. Whenever a @Published variable changes, the radio transmitter screams out to the app: *"I CHANGED! ANY VIEW WATCHING ME NEEDS TO REDRAW!"*
swift
123456789101112131415161718192021222324252627
import Foundation

// VIEWMODELS are always Classes!
class UserViewModel: ObservableObject {
    
    // The View will monitor this array.
    @Published var users: [User] = []
    
    // An example of "Business Logic" extracted away from the UI.
    func fetchFakeData() {
        // Pretend we are downloading this from the internet...
        let downloadedUsers = [
            User(username: "Alice_Dev", followers: 1500),
            User(username: "Bob_Codes", followers: 20)
        ]
        
        // Updating the @Published variable triggers the UI to redraw instantly!
        self.users = downloadedUsers
    }
    
    func addFollower(to index: Int) {
        // Complex logic handled purely in the ViewModel!
        let oldUser = users[index]
        let newUser = User(username: oldUser.username, followers: oldUser.followers + 1)
        users[index] = newUser
    }
}

6. Step 3: The View (The Face)

The View is now incredibly clean. We spawn the ViewModel using @StateObject. This ensures the Class survives even if the View struct is destroyed and redrawn!
swift
12345678910111213141516171819202122232425262728293031
import SwiftUI

struct UserListView: View {
    // We instantiate the Brain! 
    // @StateObject ensures the class is kept alive in memory.
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.users.indices, id: \.self) { index in
                HStack {
                    Text(viewModel.users[index].username)
                    Spacer()
                    Text("\(viewModel.users[index].followers) followers")
                        .foregroundColor(.gray)
                    
                    Button("Follow") {
                        // The View does ZERO math. It just asks the ViewModel to do it!
                        viewModel.addFollower(to: index)
                    }
                    .buttonStyle(.bordered)
                }
            }
            .navigationTitle("Users")
            // When the screen appears, ask the Brain to fetch the data!
            .onAppear {
                viewModel.fetchFakeData()
            }
        }
    }
}

7. @StateObject vs @ObservedObject

This is a massive interview question. Both wrappers are used to connect a View to an ObservableObject.
  • @StateObject: Use this EXACTLY ONCE when you are *creating* the ViewModel from scratch (= UserViewModel()). It tells SwiftUI to own and protect this class in memory.
  • @ObservedObject: Use this if a Parent View created the class, and is *passing* it down into a Child view. If you use @ObservedObject to create a class from scratch, SwiftUI might randomly delete it during a render cycle, causing massive bugs!

8. Visual Learning: MVVM Flow

txt
123456789101112
[ The View (UI) ]
  |-- (Button Click) --> Calls viewModel.fetchData()
  
[ The ViewModel (Brain) ]
  |-- Executes networking logic.
  |-- Retrieves raw [ Model ] data.
  |-- Updates @Published var users.
  |-- (Radio Broadcast!) --> "Hey View, I updated!"
  
[ The View (UI) ]
  |-- Hears the broadcast.
  |-- Redraws the <List> with the new data!

9. Common Mistakes

  • Putting Logic in the View: If you have an if / else statement inside a Button action that checks a user's password length, performs an MD5 hash, and filters an array... your MVVM architecture has failed. The Button action should literally just be viewModel.loginPressed(). All that complex code belongs in the ViewModel file.
  • Forgetting @Published: If you add an array to a ViewModel, but forget the @Published wrapper, the ViewModel will update its internal data perfectly, but it won't broadcast the change. The View will sit there frozen forever.

10. Best Practices

  • Folder Structure: In Xcode, you should create three distinct folders (Groups): Models, Views, and ViewModels. Keep your files strictly organized. User.swift goes in Models. UserViewModel.swift goes in ViewModels. UserListView.swift goes in Views.

11. Exercises

  1. 1. Create a Model struct Product.
  1. 2. Create an ObservableObject class ProductViewModel containing a @Published var products = [Product]() and a method addProduct().

12. Coding Challenges

Challenge: Connect a View to the ProductViewModel. Implement a List displaying the products. Add a button to the Navigation toolbar that calls the addProduct() method on the ViewModel. Observe the clean separation of UI and Logic!

13. MCQ Quiz with Answers

Question 1

In the MVVM architecture, which layer is strictly responsible for performing network requests, executing mathematical business logic, and preparing data arrays for visual presentation?

Question 2

When a SwiftUI View needs to instantiate a brand new instance of an ObservableObject class (meaning it is the absolute owner of that class), which property wrapper must be utilized to prevent unexpected memory deletion?

14. Interview Questions

  • Q: Describe the architectural flow of MVVM. How does the ObservableObject protocol combined with @Published properties facilitate the reactive binding between the ViewModel and the View?
  • Q: A developer uses @ObservedObject private var viewModel = MyViewModel() inside a view. Why is this structurally dangerous compared to @StateObject? What will eventually happen during a complex view lifecycle?
  • Q: Explain why "Massive View Controller" syndrome was so prevalent in legacy UIKit development, and how SwiftUI's declarative nature combined with MVVM explicitly prevents it.

15. FAQs

Q: What if five different screens need access to the exact same UserViewModel (like the user's login state)? A: Instead of passing it down manually to every single child view, SwiftUI provides @EnvironmentObject. You inject the ViewModel into the root of the app, and any view in the entire application can magically pluck it out of the air and use it!

16. Summary

In Chapter 17, we graduated from scripting to true Enterprise Architecture. We abandoned the chaotic practice of mixing UI layout with business logic, adopting the rigid, scalable MVVM (Model-View-ViewModel) paradigm. We modeled our raw data, extracted our processing logic into distinct ObservableObject classes, and leveraged the @Published radio-broadcaster to trigger reactive UI updates. We successfully bound our visual layouts to these logic brains utilizing the protective memory wrapper @StateObject, resulting in clean, modular, and highly testable code.

17. Next Chapter Recommendation

Our ViewModel's fetchFakeData() method is currently relying on hardcoded arrays. Real apps pull live data from external servers. Proceed to Chapter 18: Networking and API Calls in Swift to connect our apps to the world.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·