Learn Swift - Architecture & Design Patterns
Series/Learn Swift/Episode 16
Episode 16 of 23

Learn Swift - Architecture & Design Patterns

This episode covers Swift application architecture: a comparison of MVC, MVVM, MVP, and Clean Architecture, protocol-oriented design with dependency injection, the coordinator pattern for navigation, and state management using Combine and SwiftUI.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

As an app grows from dozens to thousands of files, architecture determines whether changes stay easy or turn into a nightmare. Episode 16 covers architecture and design patterns in Swift: MVC, MVVM, MVP, Clean Architecture, plus supporting patterns like dependency injection, coordinator, and Combine.

No single architecture is right for every case. What you learn here are the principles that separate good architecture from bad: separation of concerns, controlled dependency direction, and easy testability.

Main Architectural Patterns

MVC and MVP

MVC (Model-View-Controller) is UIKit's native architecture: the View displays data, the Controller mediates, and the Model holds data. Its classic problem is the Massive View Controller — the controller absorbs all logic because it becomes the only place to put code.

MVP (Model-View-Presenter) moves display logic into the Presenter, with the View only forwarding events and rendering results. The Presenter can be tested without a real UI — but the View must implement a protocol, which adds boilerplate.

MVVM: The Modern Architecture

MVVM (Model-View-ViewModel) separates state and display logic into a ViewModel that doesn't depend on the View. The View observes the ViewModel and reacts to its changes:

ViewModel with Combine
import Combine
import Foundation
 
final class LoginViewModel: ObservableObject {
    @Published var email = ""
    @Published var password = ""
    @Published var pesanError: String?
 
    var bisaSubmit: Bool {
        !email.isEmpty && !password.isEmpty
    }
}

@Published var email = "" creates a property the View can observe, and var bisaSubmit provides derived state. A pure-Swift ViewModel can be tested without a UI — that's the main appeal of MVVM for SwiftUI apps.

Clean Architecture

Clean Architecture centers business rules in a core, separate from frameworks and interfaces. The outer layers (UI, database, networking) depend on the core, not the other way around. In Swift, the core is expressed with protocols and use cases, while frameworks live in the outermost layer.

Protocol-Oriented Design and Dependency Injection

Protocol-Based Design

Episode 5 introduced protocols; in architecture, protocols become design decision points. Dependencies point at abstractions, not implementations:

Protocol-based dependency injection
protocol PenyimpanCatatan {
    func simpan(_ teks: String) throws
}
 
struct PenyimpanFile: PenyimpanCatatan {
    func simpan(_ teks: String) throws {
        let url = docs.appendingPathComponent("catatan.txt")
        try teks.write(to: url, atomically: true, encoding: .utf8)
    }
}
 
struct PenyimpanInMemory: PenyimpanCatatan {
    var data: [String] = []
    func simpan(_ teks: String) throws {
        data.append(teks)
    }
}

protocol PenyimpanCatatan defines a storage contract. The ViewModel receives a PenyimpanCatatan when created — tests inject PenyimpanInMemory, production uses PenyimpanFile. This is dependency injection: dependencies are provided from outside, not created inside.

Benefits of DI

DI offers three real advantages:

  • Testability: real dependencies can be swapped for fakes.
  • Flexibility: implementations can be swapped without changing their users.
  • Clarity: an object's dependencies are explicit at initialization.

Coordinator and Navigation

Separating Navigation

A Coordinator moves navigation logic out of the View Controller, so the View Controller doesn't need to know which screen to go to next:

Simple coordinator
final class MainCoordinator {
    private let navigator: UINavigationController
 
    init(navigator: UINavigationController) {
        self.navigator = navigator
    }
 
    func mulai() {
        let vc = DaftarViewController(coordinator: self)
        navigator.pushViewController(vc, animated: true)
    }
 
    func bukaDetail(id: Int) {
        let vc = DetailViewController(id: id)
        navigator.pushViewController(vc, animated: true)
    }
}

MainCoordinator manages transitions between screens in one place. The View Controller just calls coordinator methods — navigation becomes centralized, easy to test, and easy to change. In SwiftUI, this role is often replaced by explicitly defined route values.

State Management with Combine and SwiftUI

Combine: Value Streams

Combine is a framework for processing streams of values that change over time. Its core is a Publisher that emits values and a Subscriber that receives them:

Publisher and sink
let publisher = Just("Halo Combine")
let cancellable = publisher.sink { nilai in
    print(nilai)
}

Just("Halo Combine") is a publisher that emits a single value and then completes. publisher.sink { ... } receives that value. Combine provides transformation operators like map, filter, and combineLatest that make app state flow declaratively.

SwiftUI and @StateObject

SwiftUI integrates Combine natively. Views observe ObservableObjects through property wrappers:

SwiftUI with ObservableObject
import SwiftUI
 
struct LoginView: View {
    @StateObject var viewModel = LoginViewModel()
 
    var body: some View {
        VStack {
            TextField("Email", text: $viewModel.email)
            SecureField("Password", text: $viewModel.password)
            if let pesan = viewModel.pesanError {
                Text(pesan)
            }
        }
    }
}

@StateObject var viewModel = LoginViewModel() makes the ViewModel owned by the view and keeps it alive across re-renders. Every change to an @Published property triggers a re-render of the view — this is the heart of SwiftUI state management, which we'll deepen in episode 17.

Info

The best architecture is the one that fits your context. Start simple with MVVM for small features, and adopt stricter patterns — Clean Architecture, coordinator, modularization — as the team and codebase grow. Never force architecture as dogma.

Closing

Key takeaways:

  • MVC, MVP, and MVVM divide responsibilities in different ways.
  • MVVM with ObservableObject is the foundation of SwiftUI state management.
  • Clean Architecture centralizes business rules and inverts dependency direction.
  • Protocol-based dependency injection improves testability.
  • A coordinator centralizes navigation logic in one place.
  • Combine connects publishers and subscribers for declarative state flow.

In the next episode, episode 17, we'll cover SwiftUI and declarative UI — the basics of views and layout, state management with @State, @Binding, @ObservedObject, and @EnvironmentObject, animations and transitions with adaptive UI, and integration between UIKit and SwiftUI. Your UI becomes declarative and alive!

Learn Swift - Architecture & Design Patterns | Learn Swift