Skip to Content
Course content

77: NSPredicate for Filtering

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've already spent a lot of time using the .filter { ... } method on Swift arrays. For most of your daily work, that's exactly what you should use. It's type-safe, fast, and idiomatic. But as you move deeper into the Apple ecosystem—especially when you start hitting Core Data or working with NSArray—you're going to run into NSPredicate.

Think of NSPredicate as a way to define a logical rule as a piece of data. Instead of writing a closure, you're essentially writing a "query" that can be passed around and evaluated. It's incredibly powerful for complex filtering logic that might change at runtime.

Defining our Task model

Let's build a simple task filtering system. I want to be able to filter a list of tasks based on whether they are completed and whether the title contains a specific keyword. First, let's set up our data. I'll use a class here because NSPredicate relies on Objective-C dynamism to inspect properties at runtime.

class TodoTask: NSObject {
    @objc var title: String
    @objc var isCompleted: Bool
    @objc var priority: Int

    init(title: String, isCompleted: Bool, priority: Int) {
        self.title = title
        self.isCompleted = isCompleted
        self.priority = priority
    }
}

let tasks = [
    TodoTask(title: "Buy milk", isCompleted: false, priority: 1),
    TodoTask(title: "Finish Swift lesson", isCompleted: true, priority: 3),
    TodoTask(title: "Call the bank", isCompleted: false, priority: 2),
    TodoTask(title: "Clean the kitchen", isCompleted: false, priority: 1)
]

Filtering by simple equality

Now, let's say I only want the tasks that aren't finished yet. With a standard Swift filter, I'd just check !$0.isCompleted. With NSPredicate, I use a format string. It looks a bit like SQL, which some people love and others hate.

let pendingPredicate = NSPredicate(format: "isCompleted == %@", NSNumber(value: false))
let pendingTasks = (tasks as NSArray).filtered(using: pendingPredicate) as! [TodoTask]

// result: Buy milk, Call the bank, Clean the kitchen

Notice the %@. That's a placeholder. Never hardcode values directly into the format string if you can avoid it; using placeholders prevents "injection" style bugs and handles type conversion more gracefully. I also cast the Swift array to NSArray because the filtered(using:) method lives there.

Adding some fuzzy search logic

In a real app, users usually type into a search bar. I want to find tasks that contain a certain word, regardless of whether they used capital letters or not. This is where NSPredicate actually starts to outshine basic Swift filters because of operators like CONTAINS[cd].

The [c] means case-insensitive, and the [d] means diacritic-insensitive (so "é" matches "e").

let searchText = "MILK"
let searchPredicate = NSPredicate(format: "title CONTAINS[cd] %@", searchText)
let searchResults = (tasks as NSArray).filtered(using: searchPredicate) as! [TodoTask]

// result: Buy milk

Wait, why isn't this filtering?

Here is where I usually trip up when I'm moving quickly. I tried to add a priority filter, but I wrote it like this:

// MY MISTAKE:
let priorityPredicate = NSPredicate(format: "priority == 2") 
// This actually works for constants, but wait... 
// what if I use a variable?
let minPriority = 2
let badPredicate = NSPredicate(format: "priority == minPriority") 

I spent five minutes wondering why badPredicate wasn't returning anything. The problem is that NSPredicate thinks "minPriority" is the name of a property on the TodoTask object, not a reference to the Swift variable minPriority. It's searching for a property that doesn't exist.

To fix this, I have to use the %@ placeholder to pass the actual value of the variable into the predicate.

// THE FIX:
let minPriority = 2
let fixedPredicate = NSPredicate(format: "priority == %@", NSNumber(value: minPriority))

Combining rules with Compound Predicates

Most of the time, you don't just want one filter; you want several. You might want "incomplete tasks" AND "high priority". Instead of nesting filters, we can use NSCompoundPredicate.

let isPending = NSPredicate(format: "isCompleted == %@", NSNumber(value: false))
let isHighPriority = NSPredicate(format: "priority > %@", NSNumber(value: 1))

let combinedPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [isPending, isHighPriority])
let urgentTasks = (tasks as NSArray).filtered(using: combinedPredicate) as! [TodoTask]

// result: Call the bank

It's a bit more verbose than Swift's &&, but the beauty of this approach is that you can dynamically build an array of predicates based on which checkboxes a user has clicked in your UI, and then smash them together into one single query.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.