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")