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 class—finalpenting di sini karena@MainActorclass yang di-subclass bisa menimbulkan masalah isolation inheritance. Selalufinaluntuk 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—@MainActormenggantikannya sepenuhnya dengan cara yang type-safe dan terverifikasi compiler.