Section 4/131 menit

4. Actor & @MainActor

4. Actor & @MainActor

Teori: Isolation Berbasis Tipe

Actor memproteksi state-nya dengan memastikan hanya satu Task yang mengaksesnya pada satu waktu. Tidak perlu lock manual — compiler dan runtime yang menjamin ini.

swift
Thread A ──► await cache.value ──► (antri)
                                         │
Thread B ──► await cache.value ──► (antri)
                                         │
                                   Actor (serial access)
                                         │
                               Task A berjalan dulu
                               Task B berjalan setelah A selesai

actor — Custom Actor

swift
// Tanpa actor — DATA RACE! Bisa crash atau corrupt data
class UnsafeImageCache {
    private var store: [String: UIImage] = [:]  // ← bisa diakses dari mana saja!

    func store(_ image: UIImage, for key: String) {
        store[key] = image  // ← tidak thread-safe
    }

    func image(for key: String) -> UIImage? {
        return store[key]
    }
}

// DENGAN actor — compiler menjamin thread safety
actor ImageCache {
    private var store: [String: UIImage] = [:]

    // Akses ke state hanya bisa lewat await dari luar actor
    func store(_ image: UIImage, for key: String) {
        store[key] = image
    }

    func image(for key: String) -> UIImage? {
        return store[key]
    }

    // nonisolated — tidak butuh await saat dipanggil, tidak akses state actor
    nonisolated var description: String {
        return "ImageCache"
    }
}

// Cara menggunakan
let cache = ImageCache()

Task {
    // await wajib karena ImageCache adalah actor
    await cache.store(image, for: "avatar_123")
    let img = await cache.image(for: "avatar_123")
}

@MainActor — Isolation ke Main Thread

@MainActor adalah global actor bawaan Swift yang mewakili main thread. Tandai class/property/function dengan @MainActor untuk memastikan dijalankan di main thread.

swift
// @MainActor pada class — semua properti dan method berjalan di main thread
@MainActor
class ProfileViewModel {
    // Properti ini otomatis di-isolate ke main thread
    var userName: String = ""
    var isLoading: Bool = false
    var errorMessage: String? = nil

    private let service: NetworkService

    init(service: NetworkService) {
        self.service = service
    }

    // Method ini berjalan di main thread — aman update UI langsung
    func fetchProfile(userId: String) async {
        isLoading = true
        errorMessage = nil

        do {
            // service.fetchUser adalah async func — suspension point
            // setelah await kembali, kita sudah di main thread lagi
            let user = try await service.fetchUser(id: userId)
            userName = user.name
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }

    // nonisolated untuk pure computation yang tidak sentuh state @MainActor
    nonisolated func formatDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }
}

isolated Parameter — Inject Isolation

swift
// Fungsi yang menerima actor sebagai parameter terisolasi
func updateUI(on viewController: isolated UIViewController & AnyObject,
              with data: UserData) {
    // Di sini kita isolated ke viewController
    // Bisa akses properti viewController tanpa await
}

// Lebih umum: inject @MainActor
@MainActor
func updateLabels(name: String, email: String) {
    nameLabel.text = name
    emailLabel.text = email
}

Perbedaan Swift 5.x vs Swift 6

swift
// Di Swift 5.x: ini menghasilkan WARNING (tidak error)
class MyViewController: UIViewController {
    var items: [String] = []  // Tidak ada isolation

    func loadFromBackground() {
        Task.detached {
            // ⚠️ Warning: crossing actor boundary tanpa await
            // Di Swift 6 ini adalah ERROR
            self.items = ["A", "B"]  // Akses main thread property dari background
        }
    }
}

// Solusi yang benar di Swift 5.x (dan Swift 6):
@MainActor
class MyViewController: UIViewController {
    var items: [String] = []

    func loadFromBackground() {
        Task.detached {
            let newItems = await fetchItems()
            // Kembali ke MainActor untuk update state
            await MainActor.run {
                self.items = newItems
            }
        }
    }
}