Section 16/212 menit
16. Observable dalam VIP UIKit — Motivasi & Arsitektur
16. Observable dalam VIP UIKit — Motivasi & Arsitektur
Masalah yang Ingin Dipecahkan
Pada VIP murni (section 1–10), Presenter berkomunikasi ke ViewController melalui protocol delegate:
swift
// Pure VIP — Presenter memanggil ViewController secara langsung
@MainActor
final class UserListPresenter: UserListPresentationLogic {
weak var viewController: (any UserListDisplayLogic)? // ← weak untuk cegah retain cycle
func presentUsers(_ response: UserList.FetchUsers.Response) {
// format...
viewController?.displayUsers(viewModel) // ← panggil method VC langsung
}
}
Pendekatan ini solid, tapi ada beberapa friction:
- Retain cycle — Presenter harus menyimpan
weak var viewController. Jika lupaweak, terjadi retain cycle. Kalauweak, bisa jadinildi waktu yang tidak terduga. - Coupling implisit — Presenter harus tahu kapan memanggil method mana. Urutan pemanggilan penting:
presentLoading(true)harus sebelumpresentUsers, harus sebelumpresentLoading(false). Tidak ada enforcement dari compiler. - Testing Presenter membutuhkan
MockViewController— kita perlu membuat spy/mock yang merekam method call, padahal yang sebenarnya ingin kita uji adalah: "apakah data diformat dengan benar?"
Ide: Ganti Protocol Delegate dengan @Observable DisplayState
Alih-alih Presenter memanggil ViewController, kita ubah menjadi: Presenter memperbarui state, ViewController mengobservasi state.
swift
Pure VIP:
Interactor ──await──▶ Presenter ──memanggil──▶ ViewController
(push model: Presenter aktif memutuskan kapan VC di-update)
Observable VIP:
Interactor ──await──▶ Presenter ──update──▶ DisplayState ◀──observe── ViewController
(state model: VC reaktif, otomatis update saat state berubah)
Arsitektur Baru: DisplayState Pattern
swift
┌─────────────────────────────────────────────────────────────────────┐
│ @MainActor │
│ ┌──────────────────┐ ┌──────────────────────────────────┐ │
│ │ ViewController │─────────▶│ Router │ │
│ │ (UI only) │ │ (navigasi UIKit) │ │
│ └────────┬─────────┘ └──────────────────────────────────┘ │
│ │ observes (withObservationTracking) │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ UserListDisplayState (@Observable) │ │
│ │ │ │
│ │ var displayedUsers: [DisplayedUser] │ │
│ │ var isLoading: Bool │ │
│ │ var pendingError: String? ← one-time event │ │
│ │ var pendingNavigation: ViewModel? ← one-time event │ │
│ └───────────────────▲──────────────────────────────────────────┘ │
│ │ update properties │
│ ┌───────────────────┴──────────────────────────────────────────┐ │
│ │ Presenter (@MainActor) │ │
│ │ let displayState = UserListDisplayState() │ │
│ │ ← TIDAK ada weak var viewController lagi │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
▲ await presenter.presentUsers(response)
│
┌───────────┴──────────────────────────────────────────────────────┐
│ actor Interactor │
│ (business logic, tidak berubah dari pure VIP) │
└──────────────────────────────────────────────────────────────────┘
Perubahan Per Komponen
| Komponen | Pure VIP | Observable VIP |
|---|---|---|
| Presenter | weak var viewController?, panggil method langsung |
let displayState, update property |
| ViewController | Implement UserListDisplayLogic protocol |
Observe displayState via withObservationTracking |
| Protocol | UserListDisplayLogic penuh |
UserListDisplayLogic bisa dihapus atau disederhanakan |
| Configurator | presenter.viewController = viewController |
viewController.configure(displayState: presenter.displayState) |
| Retain cycle | Perlu weak |
Tidak ada — VC tidak di-hold oleh Presenter |
| iOS minimum | iOS 15 (async/await) | iOS 17 (@Observable) |