Section 6/211 menit

6. Presenter — @MainActor

6. Presenter — @MainActor

swift
import UIKit

// Presenter bertanggung jawab satu hal: format data untuk tampil di View.
// @MainActor karena:
// 1. Memanggil ViewController yang @MainActor
// 2. Method-nya dipanggil dari Interactor (actor) dengan await — Swift
//    otomatis menjadwalkan eksekusi ke main thread
// Tidak ada business logic di sini. Tidak ada network call. Hanya format string.
@MainActor
final class UserListPresenter: UserListPresentationLogic {

    // weak untuk mencegah retain cycle (Presenter → ViewController → Presenter).
    weak var viewController: (any UserListDisplayLogic)?

    // MARK: - UserListPresentationLogic

    // Method ini berjalan di @MainActor (main thread).
    // Ketika Interactor memanggil 'await presenter?.presentUsers(response)',
    // Swift menjadwalkan eksekusi method ini ke main thread secara otomatis.
    func presentUsers(_ response: UserList.FetchUsers.Response) {
        // map di sini adalah pure transformation — tidak ada side effect.
        // Ini adalah CPU work ringan, aman dilakukan di main thread.
        let displayedUsers = response.users.map { user in
            UserList.FetchUsers.ViewModel.DisplayedUser(
                id: user.id,
                // Presenter yang memutuskan format tampilan — bukan View, bukan Interactor.
                fullName: user.name
                    .split(separator: " ")
                    .map { $0.capitalized }
                    .joined(separator: " "),
                emailLabel: user.email.lowercased(),
                avatarURL: user.avatarURL
            )
        }

        let viewModel = UserList.FetchUsers.ViewModel(displayedUsers: displayedUsers)

        // Langsung panggil View — kita sudah di @MainActor, tidak butuh await.
        viewController?.displayUsers(viewModel)
    }

    func presentLoading(_ isLoading: Bool) {
        viewController?.displayLoading(isLoading)
    }

    func presentError(_ error: Error) {
        // Presenter memutuskan pesan error yang user-friendly.
        // LocalizedError sudah ditangani Worker — kita tinggal ambil deskripsinya.
        let message = error.localizedDescription
        viewController?.displayError(message: message)
    }

    func presentSelectedUser(_ response: UserList.SelectUser.Response) {
        let viewModel = UserList.SelectUser.ViewModel(
            userId: response.selectedUser.id,
            userName: response.selectedUser.name
        )
        viewController?.displaySelectedUser(viewModel)
    }
}

Penjelasan poin penting:

  • @MainActor final classfinal penting di sini karena @MainActor class yang di-subclass bisa menimbulkan masalah isolation inheritance. Selalu final untuk Presenter dan ViewController dalam VIP.
  • Format string ada di Presenter, bukan di View. View hanya membaca viewModel.fullName — tidak perlu tahu cara formatnya. Ini memudahkan testing: kita bisa unit test Presenter tanpa UIKit sama sekali.
  • Tidak ada DispatchQueue.main.async@MainActor menggantikannya sepenuhnya dengan cara yang type-safe dan terverifikasi compiler.