Episode ini membahas arsitektur aplikasi Swift: perbandingan MVC, MVVM, MVP, dan Clean Architecture, protocol-oriented design dengan dependency injection, pola coordinator untuk navigation, serta state management menggunakan Combine dan SwiftUI.

Saat aplikasi tumbuh dari puluhan menjadi ribuan file, arsitektur menentukan apakah perubahan tetap terasa ringan atau berubah menjadi mimpi buruk. Episode 16 membahas architecture dan design patterns di Swift: MVC, MVVM, MVP, Clean Architecture, serta pola pendukung seperti dependency injection, coordinator, dan Combine.
Tidak ada arsitektur yang benar untuk semua kasus. Yang kalian pelajari di sini adalah prinsip yang membedakan arsitektur baik dari buruk: pemisahan tanggung jawab, arah dependensi yang terkendali, dan pengujian yang mudah.
MVC (Model-View-Controller) adalah arsitektur bawaan UIKit: View menampilkan data, Controller memediasi, dan Model memegang data. Masalah klasiknya adalah Massive View Controller — controller menyerap semua logika karena jadi satu-satunya tempat menaruh kode.
MVP (Model-View-Presenter) memindahkan logika tampilan ke Presenter, dengan View hanya meneruskan event dan menampilkan hasil. Presenter bisa diuji tanpa UI nyata — tetapi View harus menerapkan protocol yang menambah boilerplate.
MVVM (Model-View-ViewModel) memisahkan state dan logika tampilan ke ViewModel yang tidak bergantung pada View. View mengamati ViewModel dan merespons perubahannya:
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 = "" membuat properti yang bisa diamati oleh View, dan var bisaSubmit menyediakan state turunan. ViewModel murni Swift bisa diuji tanpa UI — inilah daya tarik utama MVVM untuk aplikasi SwiftUI.
Clean Architecture memusatkan aturan bisnis di inti, terpisah dari framework dan antarmuka. Lapisan luarnya (UI, database, jaringan) bergantung pada inti, bukan sebaliknya. Di Swift, inti diekspresikan dengan protocol dan use case, sementara framework berdiam di lapisan terluar.
Episode 5 memperkenalkan protocol; dalam arsitektur, protocol menjadi titik keputusan desain. Dependency diarahkan ke abtraksi, bukan implementasi:
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 mendefinisikan kontrak penyimpanan. ViewModel menerima PenyimpanCatatan saat dibuat — test menyuntikkan PenyimpanInMemory, produksi memakai PenyimpanFile. Ini dependency injection: dependensi diberikan dari luar, bukan diciptakan di dalam.
DI menawarkan tiga keuntungan nyata:
Coordinator memindahkan logika navigasi keluar dari View Controller, sehingga View Controller tidak perlu tahu harus berpindah ke layar apa:
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 mengatur transisi antar layar di satu tempat. View Controller cukup memanggil metode coordinator — navigasi menjadi terpusat, mudah diuji, dan mudah diubah. Di SwiftUI, peran ini sering digantikan oleh nilai route yang terdefinisi secara eksplisit.
Combine adalah framework untuk memproses aliran nilai berubah seiring waktu. Intinya adalah Publisher yang memancarkan nilai dan Subscriber yang menerimanya:
let publisher = Just("Halo Combine")
let cancellable = publisher.sink { nilai in
print(nilai)
}Just("Halo Combine") adalah publisher yang memancarkan satu nilai lalu selesai. publisher.sink { ... } menerima nilai tersebut. Combine menyediakan operator transformasi seperti map, filter, dan combineLatest yang membuat state app mengalir secara deklaratif.
SwiftUI mengintegrasikan Combine secara native. View mengamati ObservableObject melalui property wrapper:
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() membuat ViewModel dimiliki view dan tetap hidup saat view di-render ulang. Setiap @Published yang berubah memicu ulang rendering view — inilah inti state management SwiftUI yang akan kita dalami di episode 17.
Info
Arsitektur terbaik adalah yang sesuai konteks. Mulai sederhana dengan MVVM untuk fitur kecil, dan adopsi pola yang lebih ketat — Clean Architecture, coordinator, modularization — saat tim dan codebase tumbuh. Jangan pernah memaksakan arsitektur sebagai dogma.
Inti yang harus dibawa pulang:
Di episode 17 selanjutnya kita akan membahas SwiftUI dan declarative UI — dasar views dan layout, state management dengan @State, @Binding, @ObservedObject, dan @EnvironmentObject, animasi dan transisi dengan adaptive UI, serta integrasi antara UIKit dan SwiftUI. UI kalian akan menjadi deklaratif dan hidup!