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
    }
}