MVVM Architecture in Swift


MVVM Architecture in Swift – A Complete Guide with Examples

MVVM (Model-View-ViewModel) is a design pattern that improves upon MVC by decoupling the UI from business logic. It makes Swift applications more modular, testable, and scalable.

Introduction

MVVM (Model-View-ViewModel) is a design pattern that improves upon MVC by decoupling the UI from business logic. It makes Swift applications more modular, testable, and scalable.

MVVM Components

Model (M) – Represents the data and business logic.
View (V) – Handles UI elements and user interactions.
ViewModel (VM) – Acts as an intermediary between Model and View, formatting data for presentation.

1. Understanding MVVM vs. MVC


Feature MVC MVVM
UI Updates Controller updates View ViewModel binds data to View
Testability Difficult to test UI logic Easier to test business logic
Reusability Controller tightly coupled with View ViewModel is reusable
Data Binding Manual updates Uses Bindings or Combine

2. Implementing MVVM in Swift


Let's create an MVVM-based User Profile App.


Step 1: Create the Model (Data Layer)

struct User {
    let name: String
    let age: Int
}

✔ Represents data only, without UI logic.

Step 2: Create the ViewModel (Data Processing Layer)

import Foundation

class UserViewModel {
    private let user: User

    init(user: User) {
        self.user = user
    }

    var displayName: String {
        return "Name: \(user.name)"
    }

    var displayAge: String {
        return "Age: \(user.age)"
    }
}

✔ Processes raw data into a format ready for the View.
✔ The ViewModel does not know about the View.

Step 3: Create the View (UI Layer)

import UIKit

class UserView: UIView {
    let nameLabel = UILabel()
    let ageLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupView() {
        nameLabel.frame = CGRect(x: 20, y: 50, width: 200, height: 20)
        ageLabel.frame = CGRect(x: 20, y: 80, width: 200, height: 20)
        addSubview(nameLabel)
        addSubview(ageLabel)
    }

    func updateView(with viewModel: UserViewModel) {
        nameLabel.text = viewModel.displayName
        ageLabel.text = viewModel.displayAge
    }
}

✔ The View only handles UI updates and does not process data.

Step 4: Create the ViewController (Handles UI Events)

import UIKit

class UserController: UIViewController {
    let userView = UserView()
    let viewModel = UserViewModel(user: User(name: "John Doe", age: 25))

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupView()
    }

    func setupView() {
        userView.frame = view.bounds
        view.addSubview(userView)
        userView.updateView(with: viewModel)
    }
}

✔ The Controller does not handle data directly.
✔ The ViewModel bridges the View and Model.

3. Running the MVVM App

Modify SceneDelegate.swift:

window?.rootViewController = UserController()

Output:

Name: John Doe
Age: 25

✔ MVVM keeps data, UI, and logic separate, making the app more scalable and testable.

4. Real-World Example: To-Do List with MVVM


Step 1: Model (ToDoItem.swift)

struct ToDoItem {
    let title: String
    var isCompleted: Bool
}

Step 2: ViewModel (ToDoViewModel.swift)

class ToDoViewModel {
    private var items: [ToDoItem] = [
        ToDoItem(title: "Buy groceries", isCompleted: false),
        ToDoItem(title: "Call Mom", isCompleted: true)
    ]

    func getItems() -> [ToDoItem] {
        return items
    }

    func toggleItem(at index: Int) {
        items[index].isCompleted.toggle()
    }
}

Step 3: ViewController (ToDoViewController.swift)

import UIKit

class ToDoViewController: UITableViewController {
    let viewModel = ToDoViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.getItems().count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = viewModel.getItems()[indexPath.row]
        cell.textLabel?.text = item.title
        cell.accessoryType = item.isCompleted ? .checkmark : .none
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        viewModel.toggleItem(at: indexPath.row)
        tableView.reloadData()
    }
}

Output:

Call Mom
⬜ Buy groceries

✔ MVVM ensures separation between UI and logic.

5. Data Binding in MVVM Using Combine

MVVM can be enhanced with Combine for reactive programming.

import Combine

class UserViewModel: ObservableObject {
    @Published var name: String = "John Doe"
}

Binding View to ViewModel

class UserView: UIView {
    var viewModel: UserViewModel
    private var cancellables = Set<AnyCancellable>()

    init(viewModel: UserViewModel) {
        self.viewModel = viewModel
        super.init(frame: .zero)
        bindViewModel()
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }

    func bindViewModel() {
        viewModel.$name.sink { [weak self] newName in
            self?.nameLabel.text = newName
        }.store(in: &cancellables)
    }
}

✔ Automatic UI updates when data changes.

6. Advantages of Using MVVM in Swift


  • Improved Code Structure – Separates concerns efficiently.
  • Better Testability – ViewModel is easy to test without UI dependencies.
  • Reusable Components – ViewModel can be shared across multiple Views.
  • Data Binding Support – Works with Combine & SwiftUI for reactive programming.
  • Scalability – Suitable for complex Swift applications.

Conclusion

MVVM is an evolution of MVC, making Swift applications more maintainable, scalable, and testable. It separates UI from business logic, leading to a cleaner and more reusable codebase.