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 ini actor, pengecekan isLoading dan assignment-nya adalah atomik. Tidak ada race condition. Dua Task yang memanggil fetchUsers() bersamaan dijamin salah satunya akan return di baris ini.
  • await presenter?.presentLoading(true) — ini adalah momen penting: kita menyeberangi isolation boundary dari actor Interactor ke @MainActor Presenter. Swift menjadwalkan ini ke main thread secara otomatis.
  • nonisolated(unsafe) var selectedUser — trade-off yang disengaja. Karena UserListDataStore perlu diakses secara nonisolated oleh Router, kita perlu nonisolated(unsafe). Ini aman hanya karena kita tahu Router membaca nilai ini selalu setelah Interactor selesai menulisnya (urutan dijamin oleh VIP flow).