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:

  1. Retain cycle — Presenter harus menyimpan weak var viewController. Jika lupa weak, terjadi retain cycle. Kalau weak, bisa jadi nil di waktu yang tidak terduga.
  2. Coupling implisit — Presenter harus tahu kapan memanggil method mana. Urutan pemanggilan penting: presentLoading(true) harus sebelum presentUsers, harus sebelum presentLoading(false). Tidak ada enforcement dari compiler.
  3. 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)