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:
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:
⬜ 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.