Section 4/202 menit
4. @MainActor
4. @MainActor
Teori: Global Actor dan Main Thread Contract
@MainActor adalah global actor — sebuah singleton actor yang merepresentasikan main thread. Ini bukan sekadar "jalankan di main thread" — ini adalah isolation domain yang di-enforce compiler.
Mengapa main thread istimewa?
UIKit dan AppKit tidak thread-safe. Mereka berkomunikasi dengan window server (daemon OS yang mengatur tampilan visual) melalui main run loop. Jika kamu update UI dari background thread, dua hal bisa terjadi:
- Corrupted UI state (karena concurrent writes ke UIKit internal state)
- Crash dengan "UI API called from background thread" di debug
@MainActor mengkodekan invariant ini ke dalam type system: "kode ini dijamin berjalan di main thread, compiler memverifikasinya."
Cara Kerja
swift
// @MainActor class: semua property dan method diisolasi ke main thread
@MainActor
class UserListViewModel: ObservableObject {
@Published var users: [User] = [] // dijamin di main thread
@Published var isLoading = false // dijamin di main thread
func loadUsers() async {
isLoading = true // di main thread ✓
// await: hop ke background untuk fetch
let fetched = await userRepository.fetchAll()
// Kembali ke main thread setelah await
users = fetched // di main thread ✓
isLoading = false // di main thread ✓
}
}
Memanggil @MainActor dari Background
swift
actor DataProcessor {
weak var delegate: (any DataProcessorDelegate)?
func process(data: Data) async {
let result = await heavyComputation(data)
// Crossing boundary: actor → @MainActor
// Butuh await karena berpindah isolation domain
await delegate?.processingCompleted(result: result)
}
}
@MainActor
protocol DataProcessorDelegate: AnyObject {
func processingCompleted(result: ProcessingResult)
}
MainActor.run — One-off Hop
swift
// Untuk kode yang sebagian besar di background tapi sesekali butuh main thread
actor BackgroundWorker {
func performWork() async {
let data = await fetchFromNetwork()
let processed = processLocally(data)
// One-off update ke UI tanpa harus mark seluruh fungsi @MainActor
await MainActor.run {
NotificationCenter.default.post(
name: .dataProcessed,
object: processed
)
}
}
}
Kapan Menggunakan @MainActor
Gunakan @MainActor ketika:
- ViewController, View, UIView subclass
- ViewModel yang mengelola @Published properties
- Delegate methods yang update UI
- Timer atau notification handler yang update UI
- AppDelegate, SceneDelegate
Jangan gunakan @MainActor ketika:
- Business logic yang tidak menyentuh UI — ini akan menyebabkan UI stutter jika logic berat
- Network layer, database layer, heavy computation
- Fungsi yang sering dipanggil — setiap call membutuhkan hop ke main thread
Real Use Case: ViewModel dengan Loading State
swift
@MainActor
final class ArticleListViewModel: ObservableObject {
enum ViewState {
case idle
case loading
case loaded([Article])
case error(String)
}
@Published private(set) var state: ViewState = .idle
@Published private(set) var searchQuery = ""
private let repository: ArticleRepository
private var searchTask: Task<Void, Never>?
init(repository: ArticleRepository) {
self.repository = repository
}
func load() {
guard case .idle = state else { return }
fetchArticles()
}
func search(query: String) {
searchQuery = query
searchTask?.cancel()
guard !query.isEmpty else {
fetchArticles()
return
}
// Debounce: tunggu 300ms setelah user berhenti ketik
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
await fetchArticles(query: query)
}
}
private func fetchArticles(query: String? = nil) {
state = .loading
Task {
do {
let articles: [Article]
if let q = query {
articles = try await repository.search(query: q)
} else {
articles = try await repository.fetchAll()
}
// Kembali ke @MainActor setelah await — otomatis
state = .loaded(articles)
} catch is CancellationError {
state = .idle
} catch {
state = .error(error.localizedDescription)
}
}
}
}
struct Article: Sendable, Identifiable {
let id: String
let title: String
let content: String
}