Section 7/131 menit
7. Thread Safety & Swift 6
7. Thread Safety & Swift 6
@Observable Tidak Otomatis Thread-Safe
@Observable bukan actor dan tidak memberikan thread safety secara otomatis. ObservationRegistrar menjamin keamanan registrasi observer, tapi akses ke property dari multiple thread tetap bisa menyebabkan data race.
swift
// BERBAHAYA — tidak ada isolation
@Observable
final class CounterViewModel {
var count: Int = 0 // bisa di-mutasi dari thread mana saja
func increment() {
count += 1 // jika dipanggil dari background thread, data race!
}
}
Solusi 1: @MainActor + @Observable
Paling umum untuk ViewModel yang digunakan dari SwiftUI:
swift
// Semua akses ke ViewModel dijamin di main thread.
// Swift 6 compiler akan error jika kita mencoba mengakses
// property ini dari non-MainActor context tanpa await.
@Observable
@MainActor
final class UserViewModel {
var users: [User] = []
var isLoading: Bool = false
private let service: UserService
init(service: UserService = UserService()) {
self.service = service
}
func loadUsers() async {
isLoading = true
do {
// await melepas @MainActor isolation untuk network call
users = try await service.fetchUsers()
} catch {
// kembali ke @MainActor setelah await
}
isLoading = false
}
}
// Swift 6: tidak ada warning, tidak ada data race
struct UserListView: View {
@State private var viewModel = UserViewModel()
var body: some View {
List(viewModel.users) { user in Text(user.name) }
.task { await viewModel.loadUsers() }
}
}
Solusi 2: actor + Manual Publish ke @Observable
Ketika butuh state yang di-isolasi di non-main thread tapi hasil akhirnya perlu di-observe SwiftUI:
swift
// Actor untuk business logic — berjalan di background
actor UserService {
private var cachedUsers: [User] = []
func fetchAndCache() async throws -> [User] {
let users = try await APIClient.shared.fetchUsers()
cachedUsers = users
return users
}
}
// @Observable @MainActor sebagai "presentation layer"
@Observable
@MainActor
final class UserViewModel {
var users: [User] = []
var isLoading: Bool = false
private let service = UserService()
func loadUsers() async {
isLoading = true
do {
// Crossing isolation boundary: @MainActor → actor
let fetchedUsers = try await service.fetchAndCache()
// Kembali ke @MainActor — update observable state
users = fetchedUsers
} catch { }
isLoading = false
}
}
Solusi 3: nonisolated untuk Computed Property Berat
swift
@Observable
@MainActor
final class AnalyticsViewModel {
var rawData: [DataPoint] = []
// Komputasi berat di main thread bisa menyebabkan janky UI.
// Hitung di background, publish hasilnya.
var processedData: [ProcessedPoint] = []
func processData() async {
// Hitung di background dengan Task.detached
let result = await Task.detached(priority: .userInitiated) {
// nonisolated context — tidak di @MainActor
self.rawData.map { ProcessedPoint(from: $0) } // rawData di-capture
}.value
// Update di @MainActor
processedData = result
}
}