Section 6/132 menit

6. Global Actors

6. Global Actors

6.1 Teori: Singleton Isolation Domain

Actor biasa (actor Foo {}) membuat isolation domain per-instance — setiap instance Foo adalah domain yang berbeda. Dua instance tidak berbagi domain:

swift
actor Counter {
    var count = 0
}

let c1 = Counter()  // isolation domain #1
let c2 = Counter()  // isolation domain #2 — terpisah dari #1

Terkadang kita butuh satu domain yang digunakan bersama oleh banyak tipe. Misalnya: semua UIKit calls harus di satu thread (main thread). Kita tidak ingin setiap ViewController punya actor-nya sendiri — kita ingin semua berbagi satu domain: "main thread".

Global Actor adalah actor singleton yang bisa digunakan sebagai atribut (@MainActor, @DatabaseActor, dll) untuk menyatakan bahwa sebuah tipe atau method berbagi domain tersebut.

6.2 @MainActor — Global Actor Paling Penting

@MainActor mewakili main thread. Ini adalah global actor bawaan Swift karena kebutuhan ini sangat universal dalam pengembangan aplikasi.

Mengapa semua UI harus di main thread?

  • UIKit dan AppKit tidak thread-safe — mereka tidak dirancang untuk concurrent access
  • Main thread adalah satu-satunya thread yang berkomunikasi dengan window server (macOS/iOS)
  • Animasi, layout, dan rendering semua dikoordinasikan melalui run loop di main thread
swift
@MainActor
class ViewModel: ObservableObject {
    @Published var title: String = ""
    @Published var isLoading: Bool = false
    
    // Semua property dan method di sini dijamin di main thread
    func loadData() async {
        isLoading = true  // OK: @MainActor, pasti di main thread
        
        // fetchFromNetwork() tidak @MainActor → berjalan di background
        let result = await fetchFromNetwork()
        
        // Setelah await, kita kembali ke @MainActor context
        title = result   // OK: kembali di main thread
        isLoading = false
    }
    
    private func fetchFromNetwork() async -> String {
        // Tidak ada @MainActor annotation → background thread
        try? await Task.sleep(for: .seconds(1))
        return "Data loaded"
    }
}

Kapan gunakan @MainActor:

  • ViewController dan subclassnya — selalu
  • ViewModel yang mengelola @Published properties — selalu
  • Delegate methods yang update UI
  • Callback dari timer/notification yang perlu update UI

Kapan TIDAK gunakan @MainActor:

  • Business logic yang tidak menyentuh UI — ini akan memblokir/mengganggu responsivitas UI
  • Network layer, database layer, parsing logic — semua ini lebih baik di background actor
  • Fungsi komputasi berat — bisa menyebabkan UI freeze

6.3 Custom Global Actor

swift
// Definisi global actor — satu per domain yang diinginkan
@globalActor
actor NetworkActor {
    static let shared = NetworkActor()  // WAJIB: singleton
}

// Semua tipe yang dianotasi @NetworkActor berbagi isolation domain yang sama
@NetworkActor
class NetworkManager {
    var activeRequests: [UUID: URLSessionTask] = [:]
    
    func send(_ request: URLRequest) async throws -> Data {
        let task = URLSession.shared.dataTask(with: request)
        return Data()
    }
}

@NetworkActor
class RequestInterceptor {
    // Ini berbagi domain dengan NetworkManager
    // Akses ke NetworkManager dari sini tidak butuh await!
}

@NetworkActor
func cancelAllRequests(manager: NetworkManager) {
    // Berada di NetworkActor domain — bisa akses manager.activeRequests langsung
    manager.activeRequests.values.forEach { $0.cancel() }
    manager.activeRequests.removeAll()
}

6.4 Global Actor vs Regular Actor: Kapan Memilih Mana?

swift
Gunakan Global Actor (@DatabaseActor dll) jika:
┌──────────────────────────────────────────────────────┐
│ Banyak class/struct berbagi isolation domain yang sama│
│ Kamu ingin anotasi atribut (@DatabaseActor)           │
│ Domain punya nama konseptual yang jelas               │
│ Contoh: semua database access, semua network calls    │
└──────────────────────────────────────────────────────┘

Gunakan Regular Actor jika:
┌──────────────────────────────────────────────────────┐
│ Satu instance dengan state yang perlu dilindungi      │
│ Tidak ada kebutuhan berbagi domain dengan tipe lain   │
│ Contoh: cache instance, counter, data store           │
└──────────────────────────────────────────────────────┘

Bahaya Global Actor: Jika terlalu banyak tipe diisolasi ke satu global actor, kamu menciptakan bottleneck — semua operasi yang diisolasi harus antri di satu executor. Untuk throughput tinggi, lebih baik gunakan banyak actor instance terpisah.

6.5 Global Actor pada Partial Class/Struct

swift
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

// Hanya method tertentu yang diisolasi ke DatabaseActor
class DataRepository {
    @DatabaseActor
    func fetchUser(id: String) async -> User? {
        // berjalan di DatabaseActor
        return nil
    }
    
    @DatabaseActor
    var cachedUsers: [User] = []
    
    // Method ini berjalan di caller's context — tidak diisolasi
    func prepareQuery(id: String) -> String {
        return "SELECT * WHERE id = '\(id)'"
    }
}