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
}