Advanced Fetching Predicates, Sorting, and Aggregation

Fetching data from Core Data efficiently is critical in apps that handle large datasets or need advanced filtering and sorting. In this article, we’ll explore advanced techniques for fetching

article

Fetching data from Core Data efficiently is critical in apps that handle large datasets or need advanced filtering and sorting. In this article, we’ll explore advanced techniques for fetching data with Core Data in SwiftUI, including how to filter data using NSPredicate, sort fetched data with multiple criteria, and perform aggregation functions like counting, summing, and more.


Using NSPredicate for Advanced Filtering

Core Data allows you to filter fetched data using NSPredicate, which provides a powerful way to retrieve specific data that matches certain conditions. Predicates can be used to filter based on string comparisons, numeric comparisons, date ranges, and more. Let’s start by creating a basic fetch request using NSPredicate to filter tasks by a specific condition.

Here’s an example of fetching only the tasks that are marked as completed:



struct CompletedTasksView: View {
    @FetchRequest(
        entity: Task.entity(),
        sortDescriptors: [],
        predicate: NSPredicate(format: "isCompleted == YES")
    ) private var completedTasks: FetchedResults

    var body: some View {
        List {
            ForEach(completedTasks) { task in
                Text(task.name ?? "Unknown Task")
            }
        }
    }
}

In this example, the predicate NSPredicate(format: "isCompleted == YES") filters the Task entities to fetch only the tasks where the isCompleted attribute is set to true. You can adjust this predicate to perform more complex filtering.


Using String Matching in Predicates

String matching is a common use case for filtering data. For example, let’s fetch tasks where the name contains a certain keyword. You can use the CONTAINS[cd] keyword to perform a case-insensitive and diacritic-insensitive match:



struct SearchableTasksView: View {
    @State private var searchTerm = ""
    
    @FetchRequest private var filteredTasks: FetchedResults

    init(searchTerm: String) {
        _filteredTasks = FetchRequest(
            entity: Task.entity(),
            sortDescriptors: [],
            predicate: NSPredicate(format: "name CONTAINS[cd] %@", searchTerm)
        )
    }

    var body: some View {
        VStack {
            TextField("Search Tasks", text: $searchTerm)
                .padding()

            List {
                ForEach(filteredTasks) { task in
                    Text(task.name ?? "Unknown Task")
                }
            }
        }
    }
}

In this example, the NSPredicate is used to filter tasks where the name contains the searchTerm. The predicate "name CONTAINS[cd] %@" allows for case-insensitive and diacritic-insensitive string matching, making it more flexible for user searches.


Filtering with Multiple Conditions

You can also filter data based on multiple conditions using predicates. For example, let’s fetch tasks that are marked as completed and have a high priority:



struct HighPriorityCompletedTasksView: View {
    @FetchRequest(
        entity: Task.entity(),
        sortDescriptors: [],
        predicate: NSPredicate(format: "isCompleted == YES AND priority == %@", "High")
    ) private var tasks: FetchedResults

    var body: some View {
        List {
            ForEach(tasks) { task in
                Text("\(task.name ?? "Unknown Task") - Priority: \(task.priority ?? "N/A")")
            }
        }
    }
}

In this case, we use a compound predicate with the AND operator to filter tasks that are both completed and have a priority set to "High". You can combine conditions using AND, OR, or NOT to build complex queries.


Sorting Fetched Data with NSSortDescriptor

Sorting data is an essential feature in any data-driven app. Core Data allows you to sort fetched results using one or more NSSortDescriptor objects. You can sort data based on numeric, string, or date fields, and you can combine multiple sort criteria.

Here’s how to sort tasks by their creation date in descending order:



struct SortedTasksView: View {
    @FetchRequest(
        entity: Task.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \Task.createdAt, ascending: false)]
    ) private var tasks: FetchedResults

    var body: some View {
        List {
            ForEach(tasks) { task in
                Text(task.name ?? "Unknown Task")
            }
        }
    }
}

In this example, the tasks are sorted by their createdAt date in descending order, meaning the most recent tasks will appear first. The NSSortDescriptor allows you to specify which field to sort by and whether to sort in ascending or descending order.


Sorting with Multiple Criteria

If you need to sort data by more than one attribute, you can chain multiple sort descriptors. For example, you might want to first sort tasks by priority, and then by name within each priority level:



struct MultiSortTasksView: View {
    @FetchRequest(
        entity: Task.entity(),
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Task.priority, ascending: false),
            NSSortDescriptor(keyPath: \Task.name, ascending: true)
        ]
    ) private var tasks: FetchedResults

    var body: some View {
        List {
            ForEach(tasks) { task in
                Text("\(task.name ?? "Unknown Task") - Priority: \(task.priority ?? "N/A")")
            }
        }
    }
}

Here, the tasks are first sorted by priority in descending order, and then by name in ascending order within each priority group. This ensures that tasks with the highest priority come first, and within each priority level, they are alphabetically sorted by name.


Aggregation in Core Data

Core Data also supports aggregation functions such as COUNT, SUM, MIN, MAX, and AVERAGE. These functions allow you to perform calculations on your data directly within a fetch request.

Here’s how you can count the total number of tasks in a specific category:



func countTasksInCategory(_ category: Category, context: NSManagedObjectContext) -> Int {
    let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task")
    fetchRequest.predicate = NSPredicate(format: "category == %@", category)
    fetchRequest.resultType = .countResultType

    do {
        let countResult = try context.fetch(fetchRequest)
        return countResult.first?.intValue ?? 0
    } catch {
        print("Failed to count tasks: \(error.localizedDescription)")
        return 0
    }
}

In this example, we use a fetch request with a result type of .countResultType to count the number of tasks associated with a given category. The result is returned as a numeric value.


Summing and Averaging Values

You can also sum or average numeric values across a collection of objects. For example, let’s calculate the total price of all tasks in a given order:



func totalPriceForOrder(_ order: Order, context: NSManagedObjectContext) -> Double {
    let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task")
    fetchRequest.predicate = NSPredicate(format: "order == %@", order)
    fetchRequest.resultType = .dictionaryResultType

    let expressionDesc = NSExpressionDescription()
    expressionDesc.name = "totalPrice"
    expressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "price")])
    expressionDesc.expressionResultType = .doubleAttributeType

    fetchRequest.propertiesToFetch = [expressionDesc]

    do {
        let results = try context.fetch(fetchRequest)
        let total = results.first?["totalPrice"] as? Double ?? 0.0
        return total
    } catch {
        print("Failed to calculate total price: \(error.localizedDescription)")
        return 0.0
    }
}

In this example, we use an NSExpression to sum the price attribute for all tasks in a specific order. The total price is returned as a Double. You can similarly use aggregation functions like MIN, MAX, or AVERAGE to calculate other metrics.


Conclusion

Advanced fetching with Core Data in SwiftUI opens up powerful possibilities for filtering, sorting, and aggregating data. By mastering NSPredicate, NSSortDescriptor, and aggregation functions, you can efficiently manage and display complex datasets in your SwiftUI app. In the next article, we’ll explore performance optimizations to ensure your Core Data and SwiftUI apps run smoothly even with large datasets.


In this article, we explored advanced fetching techniques in Core Data using SwiftUI. We covered how to filter data using NSPredicate, sort results with NSSortDescriptor, and perform aggregation functions like counting and summing. These techniques are essential for efficiently managing complex datasets in your apps. In the next article, we’ll focus on optimizing Core Data performance in SwiftUI apps.

instructor

Exodai INSTRUCTOR!

Johan t'Sas

Owner and Swift developer!