Section 10/181 menit
10. Background Processing dengan @ModelActor
10. Background Processing dengan @ModelActor
@ModelActor (diperkenalkan iOS 17, distabilkan di iOS 18 dengan Swift 6) memungkinkan operasi database berat dilakukan di background thread dengan isolasi yang jelas.
swift
// MARK: - Background Actor
@ModelActor
actor DataImporter {
// modelContext sudah tersedia otomatis dari @ModelActor macro
// modelContainer juga tersedia
func importBooks(from jsonData: Data) async throws -> Int {
let rawBooks = try JSONDecoder().decode([RawBookDTO].self, from: jsonData)
var importCount = 0
for raw in rawBooks {
// Cek duplikat sebelum insert
let existingDescriptor = FetchDescriptor<Book>(
predicate: #Predicate { $0.title == raw.title && $0.author == raw.author }
)
let existing = try modelContext.fetchCount(existingDescriptor)
guard existing == 0 else { continue }
let book = Book(
title: raw.title,
author: raw.author,
publishedYear: raw.year
)
modelContext.insert(book)
importCount += 1
// Save secara berkala untuk hindari memory spike
if importCount % 100 == 0 {
try modelContext.save()
}
}
try modelContext.save()
return importCount
}
func deleteAllCompletedTasks() async throws {
let descriptor = FetchDescriptor<Task>(
predicate: #Predicate { $0.isCompleted }
)
let completed = try modelContext.fetch(descriptor)
completed.forEach { modelContext.delete($0) }
try modelContext.save()
}
func recalculateStatistics() async throws -> AppStatistics {
let totalBooks = try modelContext.fetchCount(FetchDescriptor<Book>())
let readBooksDescriptor = FetchDescriptor<Book>(
predicate: #Predicate { $0.isRead }
)
let readBooks = try modelContext.fetchCount(readBooksDescriptor)
return AppStatistics(total: totalBooks, read: readBooks)
}
}
struct AppStatistics {
let total: Int
let read: Int
var readPercentage: Double { Double(read) / Double(max(1, total)) * 100 }
}
struct RawBookDTO: Codable {
let title: String
let author: String
let year: Int
}
// MARK: - Penggunaan dari View atau ViewModel
@MainActor
class LibraryViewModel: ObservableObject {
@Published var importProgress: String = ""
@Published var stats: AppStatistics?
private let importer: DataImporter
init(container: ModelContainer) {
self.importer = DataImporter(modelContainer: container)
}
func importBooksFromJSON(_ data: Data) async {
do {
importProgress = "Mengimpor..."
let count = try await importer.importBooks(from: data)
importProgress = "Berhasil impor \(count) buku"
} catch {
importProgress = "Error: \(error.localizedDescription)"
}
}
func refreshStats() async {
stats = try? await importer.recalculateStatistics()
}
}
Passing PersistentIdentifier antar Context
Karena @Model instance di-bind ke context tertentu, untuk transfer data antar context gunakan PersistentIdentifier:
swift
@ModelActor
actor BookEnricher {
func enrichBook(id: PersistentIdentifier, with metadata: BookMetadata) async throws {
// Resolve ID ke instance di context actor ini
guard let book = self[id, as: Book.self] else { return }
book.coverImageURL = metadata.coverURL
book.pageCount = metadata.pageCount
book.isbn = metadata.isbn
try modelContext.save()
}
}
// Dari main context, ambil ID lalu pass ke actor
@MainActor
func enrichSelectedBook(_ book: Book, metadata: BookMetadata) async throws {
let bookID = book.persistentModelID // safe: ambil ID di main context
try await enricher.enrichBook(id: bookID, with: metadata)
}