Section 7/181 menit

7. @Query Lanjutan: Section, Animation, dan Pagination

7. @Query Lanjutan: Section, Animation, dan Pagination

SectionedQuery (iOS 18+)

swift
import SwiftUI
import SwiftData

struct TaskListView: View {
    // Sectioned query: task dikelompokkan berdasarkan priority
    @Query(
        filter: #Predicate<Task> { !$0.isCompleted },
        sort: [SortDescriptor(\Task.priority), SortDescriptor(\Task.dueDate)]
    )
    private var tasks: [Task]
    
    // Computed grouping dari tasks
    private var tasksByPriority: [(String, [Task])] {
        let grouped = Dictionary(grouping: tasks, by: \.priority)
        return grouped.sorted { $0.key > $1.key }  // high, medium, low
    }
    
    var body: some View {
        List {
            ForEach(tasksByPriority, id: \.0) { priority, tasks in
                Section(priority.capitalized) {
                    ForEach(tasks) { task in
                        TaskRow(task: task)
                    }
                }
            }
        }
    }
}

FetchDescriptor untuk Pagination Manual

swift
// Pagination via FetchDescriptor — lebih kontrol daripada @Query
class TaskPaginationViewModel: ObservableObject {
    @Published var tasks: [Task] = []
    @Published var isLoading = false
    
    private let pageSize = 20
    private var currentPage = 0
    private var hasMore = true
    
    private let modelContext: ModelContext
    
    init(modelContext: ModelContext) {
        self.modelContext = modelContext
    }
    
    func loadNextPage() async {
        guard !isLoading && hasMore else { return }
        
        await MainActor.run { isLoading = true }
        
        var descriptor = FetchDescriptor<Task>(
            predicate: #Predicate { !$0.isCompleted },
            sortBy: [SortDescriptor(\Task.createdAt, order: .reverse)]
        )
        descriptor.fetchLimit = pageSize
        descriptor.fetchOffset = currentPage * pageSize
        
        do {
            let newTasks = try modelContext.fetch(descriptor)
            await MainActor.run {
                tasks.append(contentsOf: newTasks)
                hasMore = newTasks.count == pageSize
                currentPage += 1
                isLoading = false
            }
        } catch {
            await MainActor.run { isLoading = false }
        }
    }
    
    func reset() {
        tasks = []
        currentPage = 0
        hasMore = true
    }
}

Fetch Count Tanpa Load Objects

swift
// Hitung jumlah record tanpa fetch semua data ke memori
func countPendingTasks(context: ModelContext) throws -> Int {
    let descriptor = FetchDescriptor<Task>(
        predicate: #Predicate { !$0.isCompleted }
    )
    return try context.fetchCount(descriptor)
}

// Cek apakah ada record yang memenuhi kondisi
func hasOverdueTasks(context: ModelContext) throws -> Bool {
    var descriptor = FetchDescriptor<Task>(
        predicate: #Predicate<Task> { task in
            !task.isCompleted && task.dueDate < Date.now
        }
    )
    descriptor.fetchLimit = 1  // Cukup cek satu saja
    return try context.fetchCount(descriptor) > 0
}

@Query dengan Animation

swift
struct AnimatedTaskList: View {
    @Query(
        filter: #Predicate<Task> { !$0.isCompleted },
        sort: \Task.createdAt,
        animation: .spring(duration: 0.3)  // Animasi saat data berubah
    )
    private var tasks: [Task]
    
    var body: some View {
        List {
            ForEach(tasks) { task in
                TaskRow(task: task)
                    .swipeActions {
                        Button("Selesai", systemImage: "checkmark") {
                            task.isCompleted = true
                        }
                        .tint(.green)
                    }
            }
        }
    }
}