Section 5/211 menit
5. Interactor — actor
5. Interactor — actor
swift
import Foundation
// Interactor adalah "otak" dari fitur — semua business logic ada di sini.
// Dijadikan actor karena:
// 1. Punya mutable state (isLoading, cachedUsers, selectedUser)
// 2. Dipanggil dari beberapa tempat secara concurrent (pull-to-refresh, auto-refresh)
// 3. Isolation memastikan state tidak corrupt saat ada concurrent calls
actor UserListInteractor: UserListBusinessLogic, UserListDataStore {
// Presenter disimpan sebagai weak reference untuk mencegah retain cycle.
// 'any' keyword karena ini existential — kita tidak tahu concrete type-nya.
// Tidak perlu Sendable karena diakses hanya dari isolation domain actor ini.
weak var presenter: (any UserListPresentationLogic)?
private let worker: UserWorker
// State internal — karena ini actor, semua akses ke property ini
// otomatis thread-safe. Tidak butuh lock atau DispatchQueue.
private var isLoading = false
private var cachedUsers: [User] = []
// UserListDataStore conformance — digunakan Router untuk mengambil
// data user yang dipilih saat navigasi.
// 'nonisolated' karena Router (@MainActor) membacanya tanpa await.
// TAPI: nonisolated + var mutable adalah berbahaya.
// Kita gunakan nonisolated(unsafe) karena Router hanya membaca
// setelah Interactor sudah selesai menulis (terurut oleh VIP flow).
nonisolated(unsafe) private(set) var selectedUser: User?
init(worker: UserWorker = UserWorker()) {
self.worker = worker
}
// MARK: - UserListBusinessLogic
func fetchUsers(request: UserList.FetchUsers.Request) async {
// Guard: hindari multiple concurrent fetch.
// Karena ini actor, pengecekan dan assignment ini atomic.
guard !isLoading else { return }
isLoading = true
// Panggil Presenter untuk tampilkan loading indicator.
// 'await' diperlukan karena presenter adalah @MainActor —
// kita menyeberangi isolation boundary dari actor ke @MainActor.
await presenter?.presentLoading(true)
do {
// Panggil Worker — ini juga actor, butuh await.
// Thread actor ini dilepas selagi menunggu network.
let users = try await worker.fetchUsers()
// Setelah await, kita kembali ke isolation domain Interactor.
// Update cache — aman karena kita satu-satunya yang bisa akses ini.
cachedUsers = users
// Kirim Response ke Presenter.
// Response adalah Sendable struct — aman melewati isolation boundary.
let response = UserList.FetchUsers.Response(users: users)
// 'await' lagi karena presenter adalah @MainActor.
await presenter?.presentUsers(response)
} catch {
await presenter?.presentError(error)
}
// Selalu matikan loading, baik sukses maupun error.
isLoading = false
await presenter?.presentLoading(false)
}
func selectUser(request: UserList.SelectUser.Request) async {
// Validasi index — cachedUsers aman diakses di sini (dalam actor).
guard request.index < cachedUsers.count else { return }
let user = cachedUsers[request.index]
// Set selectedUser untuk dibaca Router nanti.
// nonisolated(unsafe) memungkinkan ini, tapi kita tahu Router
// hanya akan membacanya setelah routing dipanggil (setelah ini).
selectedUser = user
let response = UserList.SelectUser.Response(selectedUser: user)
await presenter?.presentSelectedUser(response)
}
}
Penjelasan poin penting:
guard !isLoading else { return }— karena iniactor, pengecekanisLoadingdan assignment-nya adalah atomik. Tidak ada race condition. DuaTaskyang memanggilfetchUsers()bersamaan dijamin salah satunya akanreturndi baris ini.await presenter?.presentLoading(true)— ini adalah momen penting: kita menyeberangi isolation boundary dariactorInteractor ke@MainActorPresenter. Swift menjadwalkan ini ke main thread secara otomatis.nonisolated(unsafe) var selectedUser— trade-off yang disengaja. KarenaUserListDataStoreperlu diakses secaranonisolatedoleh Router, kita perlunonisolated(unsafe). Ini aman hanya karena kita tahu Router membaca nilai ini selalu setelah Interactor selesai menulisnya (urutan dijamin oleh VIP flow).