NEW: Join my free 100 Days of SwiftUI challenge today! >>

Dynamically filtering @FetchRequest with SwiftUI

Paul Hudson    @twostraws   

One of the SwiftUI questions I’ve been asked more than any other is this: how can I dynamically change a Core Data @FetchRequest to use a different predicate or sort order? The question arises because fetch requests are created as a property, so if you try to make them reference another property Swift will refuse.

There is a simple solution here, and it is usually pretty obvious in retrospect because it’s exactly how everything else works: we should carve off the functionality we want into a separate view, then inject values into it.

I want to demonstrate this with some real code, so I’ve put together the simplest possible example: it adds three singers to Core Data, then uses two buttons to show either singers whose last name ends in A or S.

Start by creating a new Core Data entity called Singer and give it two string attributes: “firstName” and “lastName”. Use the data model inspector to change its Codegen to Manual/None, then go to the Editor menu and select Create NSManagedObject Subclass so we can get a Singer class we can customize.

Once Xcode has generated files for us, open Singer+CoreDataProperties.swift and add these two properties that make the class easier to use with SwiftUI:

var wrappedFirstName: String {
    firstName ?? "Unknown"
}

var wrappedLastName: String {
    lastName ?? "Unknown"
}

OK, now onto the real work.

The first step is to design a view that will host our information. Like I said, this is also going to have two buttons that lets us change the way the view is filtered, and we’re going to have an extra button to insert some testing data so you can see how it works.

First, add two properties to your ContentView struct so that we have a managed object context we can save objects to, and some state we can use as a filter:

@Environment(\.managedObjectContext) var moc
@State var lastNameFilter = "A"

For the body of the view, we’re going to use a VStack with three buttons, plus a comment for where we want the List to show matching singers:

VStack {
    // list of matching singers

    Button("Add Examples") {
        let taylor = Singer(context: self.moc)
        taylor.firstName = "Taylor"
        taylor.lastName = "Swift"

        let ed = Singer(context: self.moc)
        ed.firstName = "Ed"
        ed.lastName = "Sheeran"

        let adele = Singer(context: self.moc)
        adele.firstName = "Adele"
        adele.lastName = "Adkins"

        try? self.moc.save()
    }

    Button("Show A") {
        self.lastNameFilter = "A"
    }

    Button("Show S") {
        self.lastNameFilter = "S"
    }
}

So far, so easy. Now for the interesting part: we need to replace that // list of matching singers comment with something real. This isn’t going to use @FetchRequest because we want to be able to create a custom fetch request inside an initializer, but the code we’ll be using instead is almost identical.

Create a new SwiftUI view called “FilteredList”, and give it this property:

var fetchRequest: FetchRequest<Singer>

That will store our fetch request, so that we can loop over it inside the body. However, we don’t create the fetch request here, because we still don’t know what we’re searching for. Instead, we’re going to create a custom initializer that accepts a filter string and uses that to set the fetchRequest property.

Add this initializer now:

init(filter: String) {
    fetchRequest = FetchRequest<Singer>(entity: Singer.entity(), sortDescriptors: [], predicate: NSPredicate(format: "lastName BEGINSWITH %@", filter))
}

That will run a fetch request using the current managed object context. Because this view will be used inside ContentView, we don’t even need to inject a managed object context into the environment – it will inherit the context from ContentView.

All that remains is to write the body of the view, and the only interesting thing here is that without @FetchRequest we need to read the wrappedValue property of fetchRequest to pull out our data. So, give the view this body:

var body: some View {
    List(fetchRequest.wrappedValue, id: \.self) { singer in
        Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
    }
}

If you don’t like using fetchRequest.wrappedValue, you could create a simple computed property like this:

var singers: FetchedResults<Singer> { fetchRequest.wrappedValue }

Now that the view is complete, we can return to ContentView and replace the comment with some actual code that passes our filter into FilteredList:

FilteredList(filter: lastNameFilter)

Now run the program to give it a try: tap the Add Examples button first to create three singer objects, then tap either “Show A” or “Show S” to toggle between surname letters. You should see our List dynamically update with different data, depending on which button you press.

So, it took a little new knowledge to make this work, but it really wasn’t that hard – as long as you think like SwiftUI, the solution is right there.

Want to go further?

For more flexibility, we could improve our FilteredList view so that it works with any kind of entity, and can filter on any field. To make this work properly, we need to make a few changes:

  1. Rather than specifically referencing the Singer class, we’re going to use generics with a constraint that whatever is passed in must be an NSManagedObject.
  2. We need to accept a second parameter to decide which key name we want to filter on, because we might be using an entity that doesn’t have a lastName attribute.
  3. Because we don’t know ahead of time what each entity will contain, we’re going to let our containing view decide. So, rather than just using a text view of a singer’s name, we’re instead going to ask for a closure that can be run to configure the view however they want.

There are two complex parts in there. The first is the closure that decides the content of each list row, because it needs to use two important pieces of syntax. We looked at these towards the end of our earlier technique project on views and modifiers, but if you missed them:

  • @ViewBuilder lets our containing view (whatever is using the list) send in multiple views, and our list will create an implicit HStack just like the regular List.
  • @escaping says the closure will be stored away and used later, which means Swift needs to take care of its memory.

The second complex part is how we let our container view customize the search key. Previously we controlled the filter value like this:

NSPredicate(format: "lastName BEGINSWITH %@", filter)

So you might take an educated guess and write code like this:

NSPredicate(format: "%@ BEGINSWITH %@", keyName, filter)

However, that won’t work. You see, when we write %@ Core Data automatically inserts quote marks for us so that the predicate reads correctly. This is helpful, because if our string contains quote marks it will automatically make sure they don’t clash with the quote marks it adds.

This means when we use %@ for the attribute name we might end up with a predicate like this:

NSPredicate(format: "'lastName' BEGINSWITH 'S'")

And that’s not correct: the attribute name should not be in quote marks.

To resolve this, NSPredicate has a special symbol that can be used to replace attribute names: %K, for “key”. This will insert values we provide, but won’t add quote marks around them. The correct predicate is this:

NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue)

So, replace your current FilteredList struct with this:

struct FilteredList<T: NSManagedObject, Content: View>: View {
    var fetchRequest: FetchRequest<T>
    var singers: FetchedResults<T> { fetchRequest.wrappedValue }

    // this is our content closure; we'll call this once for each item in the list
    let content: (T) -> Content

    var body: some View {
        List(fetchRequest.wrappedValue, id: \.self) { singer in
            self.content(singer)
        }
    }

    init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
        fetchRequest = FetchRequest<T>(entity: T.entity(), sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
        self.content = content
    }
}

We can now use that new filtered list by upgrading ContentView like this:

FilteredList(filterKey: "lastName", filterValue: lastNameFilter) { (singer: Singer) in
    Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
}

Notice how I’ve specifically used (singer: Singer) as the closure’s parameter – this is required so that Swift understands how FilteredList is being used. Remember, we said it could be any type of NSManagedObject, but in order for Swift to know exactly what type of managed object it is we need to be explicit.

Anyway, with that change in place we now use our list with any kind of filter key and any kind of entity – it’s much more useful!

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.5/5