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