Section 9/181 menit
9. ModelActor: Background Processing dan Batch Operations
9. ModelActor: Background Processing dan Batch Operations
Dasar ModelActor
swift
import SwiftData
// ModelActor = actor dengan private ModelContext
// Semua operasi berjalan di background thread, aman dari data race
@ModelActor
actor DataSyncActor {
// modelContext tersedia otomatis (injected oleh @ModelActor macro)
func syncFromServer(articles: [ArticleDTO]) async throws {
for dto in articles {
// Cek apakah sudah ada
let descriptor = FetchDescriptor<Article>(
predicate: #Predicate { $0.remoteID == dto.id }
)
if let existing = try modelContext.fetch(descriptor).first {
// Update existing
existing.title = dto.title
existing.body = dto.body
existing.updatedAt = dto.updatedAt
} else {
// Insert baru
let article = Article(
remoteID: dto.id,
title: dto.title,
body: dto.body
)
modelContext.insert(article)
}
}
try modelContext.save()
}
}
// Inisialisasi dan penggunaan
let syncActor = DataSyncActor(modelContainer: container)
try await syncActor.syncFromServer(articles: fetchedDTOs)
Batch Upsert dengan @Attribute(.unique)
swift
@ModelActor
actor BulkImportActor {
// Cara paling efisien untuk bulk upsert dengan unique attribute
func bulkUpsert(items: [ItemDTO]) async throws {
// @Attribute(.unique) memastikan SwiftData otomatis upsert
// Tidak perlu fetch dulu untuk cek duplikat
for item in items {
let model = Item(
remoteID: item.id, // @Attribute(.unique)
name: item.name,
price: item.price
)
modelContext.insert(model)
// Jika remoteID sudah ada: update propertinya
// Jika belum ada: insert baru
}
try modelContext.save()
}
// Batch delete
func deleteOlderThan(_ date: Date) async throws {
let descriptor = FetchDescriptor<Item>(
predicate: #Predicate { $0.createdAt < date }
)
let items = try modelContext.fetch(descriptor)
for item in items {
modelContext.delete(item)
}
try modelContext.save()
print("Deleted \(items.count) items older than \(date)")
}
// Batch update
func markAllAsRead(categoryID: String) async throws {
let descriptor = FetchDescriptor<Notification>(
predicate: #Predicate<Notification> { notif in
notif.categoryID == categoryID && !notif.isRead
}
)
let unread = try modelContext.fetch(descriptor)
for notif in unread {
notif.isRead = true
notif.readAt = Date.now
}
if !unread.isEmpty {
try modelContext.save()
}
}
}
Passing Data Antar Context dengan PersistentIdentifier
swift
// JANGAN pass model instance antar thread/context — gunakan PersistentIdentifier
@MainActor
class TaskViewModel: ObservableObject {
@Published var selectedTaskTitle: String = ""
private let modelContext: ModelContext
private let backgroundActor: TaskProcessingActor
init(modelContext: ModelContext, container: ModelContainer) {
self.modelContext = modelContext
self.backgroundActor = TaskProcessingActor(modelContainer: container)
}
func processTask(_ task: Task) async {
// ✓ Pass identifier, bukan instance
let identifier = task.persistentModelID
await backgroundActor.process(taskID: identifier)
// Setelah background selesai, fetch ulang di main context
if let updatedTask = modelContext.model(for: identifier) as? Task {
selectedTaskTitle = updatedTask.title
}
}
}
@ModelActor
actor TaskProcessingActor {
func process(taskID: PersistentIdentifier) async {
// Fetch di context actor ini menggunakan identifier
guard let task = modelContext.model(for: taskID) as? Task else { return }
task.processedAt = Date.now
task.status = "processed"
try? modelContext.save()
}
}
Deduplication Actor
swift
@ModelActor
actor DeduplicationActor {
func deduplicate<T: PersistentModel>(
type: T.Type,
groupingKeyPath: KeyPath<T, String>
) async throws -> Int {
let all = try modelContext.fetch(FetchDescriptor<T>())
var seen: [String: T] = [:]
var duplicates: [T] = []
for item in all {
let key = item[keyPath: groupingKeyPath]
if seen[key] != nil {
duplicates.append(item)
} else {
seen[key] = item
}
}
for duplicate in duplicates {
modelContext.delete(duplicate)
}
if !duplicates.isEmpty {
try modelContext.save()
}
return duplicates.count
}
}
// Penggunaan:
let actor = DeduplicationActor(modelContainer: container)
let removed = try await actor.deduplicate(
type: Contact.self,
groupingKeyPath: \.email
)
print("Removed \(removed) duplicate contacts")