Section 6/181 menit

6. #Predicate Lanjutan: Compound, Subquery, dan Traversal

6. #Predicate Lanjutan: Compound, Subquery, dan Traversal

Compound Predicate

swift
import SwiftData

// AND — kedua kondisi harus terpenuhi
let urgentAndOverdue = #Predicate<Task> { task in
    task.priority == "high" && task.dueDate < Date.now
}

// OR — salah satu kondisi terpenuhi
let highOrUrgent = #Predicate<Task> { task in
    task.priority == "high" || task.priority == "urgent"
}

// NOT
let notCompleted = #Predicate<Task> { task in
    !task.isCompleted
}

// Kombinasi kompleks
let actionRequired = #Predicate<Task> { task in
    !task.isCompleted &&
    (task.priority == "high" || task.dueDate < Date.now) &&
    task.assignedTo != nil
}

Relationship Traversal

swift
// Filter Post berdasarkan properti Author-nya
let descriptor = FetchDescriptor<Post>(
    filter: #Predicate<Post> { post in
        // Traversal ke relasi: post.author.isVerified
        post.author?.isVerified == true
    }
)

// Filter Task berdasarkan properti relasi Tags (any/all)
let importantTasks = #Predicate<Task> { task in
    // any: minimal satu tag dengan nama "important"
    task.tags.contains { tag in
        tag.name == "important"
    }
}

// Filter Project yang semua Task-nya selesai
let completedProjects = #Predicate<Project> { project in
    // Semua task dalam project sudah selesai
    project.tasks.allSatisfy { task in
        task.isCompleted
    }
}

// Nested relationship traversal
let articlesFromVerifiedAuthorsInTechCategory = #Predicate<Article> { article in
    article.author?.isVerified == true &&
    article.categories.contains { category in
        category.name == "Technology"
    }
}

String Matching Predicates

swift
// Exact match
let exact = #Predicate<Contact> { $0.email == "user@example.com" }

// Case-insensitive contains (localizedStandardContains)
let searchPredicate = #Predicate<Contact> { contact in
    contact.name.localizedStandardContains("john")
}

// Prefix / Suffix
let startsWithA = #Predicate<Contact> { $0.name.hasPrefix("A") }

// Catatan: regex tidak didukung di #Predicate SwiftData
// Gunakan post-fetch filtering untuk pattern kompleks:
let contacts = try context.fetch(FetchDescriptor<Contact>())
let filtered = contacts.filter { $0.phone?.range(of: #"^\+62"#, options: .regularExpression) != nil }

Date Range Predicate

swift
// Filter dalam rentang tanggal
let calendar = Calendar.current
let startOfToday = calendar.startOfDay(for: Date.now)
let endOfToday = calendar.date(byAdding: .day, value: 1, to: startOfToday)!

let todaysTasks = #Predicate<Task> { task in
    task.dueDate >= startOfToday && task.dueDate < endOfToday
}

// Task yang due dalam 7 hari ke depan
let nextWeek = Calendar.current.date(byAdding: .day, value: 7, to: Date.now)!
let upcomingTasks = #Predicate<Task> { task in
    !task.isCompleted &&
    task.dueDate != nil &&
    task.dueDate! >= Date.now &&
    task.dueDate! <= nextWeek
}

Menggabungkan Predicate secara Programmatic

swift
// Predicate tidak bisa langsung di-compose seperti NSPredicate
// Solusi: buat FetchDescriptor dengan filter yang di-build berdasarkan kondisi

struct TaskFilter {
    var priority: String?
    var isCompleted: Bool?
    var searchText: String?
    var dueBefore: Date?
}

func makeFetchDescriptor(filter: TaskFilter) -> FetchDescriptor<Task> {
    // Build predicate dengan kondisi yang aktif
    var descriptor = FetchDescriptor<Task>()
    
    // Karena #Predicate tidak bisa di-compose dinamis,
    // gunakan closure-based filter setelah fetch untuk kondisi kompleks
    descriptor.sortBy = [SortDescriptor(\Task.dueDate, order: .forward)]
    
    // Hanya apply #Predicate untuk kondisi yang bisa di-translate ke SQL
    if let isCompleted = filter.isCompleted {
        descriptor.predicate = #Predicate<Task> { task in
            task.isCompleted == isCompleted
        }
    }
    
    return descriptor
}

// Post-fetch filter untuk kondisi yang tidak bisa di-#Predicate
func fetchTasks(filter: TaskFilter, context: ModelContext) throws -> [Task] {
    var descriptor = makeFetchDescriptor(filter: filter)
    var tasks = try context.fetch(descriptor)
    
    if let priority = filter.priority {
        tasks = tasks.filter { $0.priority == priority }
    }
    if let searchText = filter.searchText, !searchText.isEmpty {
        tasks = tasks.filter {
            $0.title.localizedCaseInsensitiveContains(searchText) ||
            $0.notes?.localizedCaseInsensitiveContains(searchText) == true
        }
    }
    if let dueBefore = filter.dueBefore {
        tasks = tasks.filter { $0.dueDate.map { $0 < dueBefore } ?? false }
    }
    
    return tasks
}