M5L4 : Sorting and Filtering

We learn in lesson 4 of module 5 from the CWC+ iOS Database course, that we can sort our core data on fetch using sortDescriptors, which is an array of keys to sort plus a parameter for whether that sort should be in ascending order or not.

In its simplest form, we use this code:

let request = Person.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "age", ascending: true)]

And we also learned that we can filter the fetched data using predicate, which contains the format of the filter.

request.predicate = NSPredicate(format: "name contains %@", filterByText)

A source of information on how we can use predicate is linked to here: Predicate Programming Guide (archive). It’s not made clear in the video, although the guide itself does, that predicate is a CocoaPods element so, presumably, what we learn here won’t hold up now that Firebase seems to be advocating using the Swift Package Manager in preference to CocoaPods. I hope this doesn’t prove to be an obsolete lesson.

At one point in the video, we’re taught about the onEditingChanged parameter of TextField, but Xcode reports that as being deprecated so it’s not going to be all that valuable right now.

The idea behind that is that, as you tap into and out of the TextField, we could use onEditingChanged to call our fetchData() method and automatically update our list of results.

Fortunately, Chris does demonstrate an alternative and, I believe, better method of achieving this. By using the onChange() property of the VStack, we can call our fetchData() method every time the user types/deletes a letter in the TextField and have the list of results instantly updated. I can see that being very handy.

Example code:

@State var people = [Person]()
@State var filterByText = ""

VStack {
    TextField("Filter Text", text: $filterByText)
        .border(.black, width: 1)
        .padding()
    List {
        ForEach(people) { person in
            Text("\(person.name ?? "No name"), age: \(String(person.age))")
        }
    }
}
.onChange(of: filterByText, perform: { value in
    fetchData()
})

And this is our fetchData() method, complete with sortDescriptors and predicate:

func fetchData() {
    let request = Person.fetchRequest()
    request.sortDescriptors = [NSSortDescriptor(key: "age", ascending: true)]
    request.predicate = NSPredicate(format: "name contains %@", filterByText)
    DispatchQueue.main.async {
        do {
            let results = try viewContext.fetch(request)
            self.people = results
        }
        catch {
            print(error.localizedDescription)
        }
    }
}

More information on how predicate works can be found at the link above, or at the Apple Developer website.

As handy reference, this would be of interest:

Case- and diacritic-insensitive lookups, such as name contains[cd] “stein”

To perform a case-insensitive search, we’d add the [c] flag, thus –

request.predicate = NSPredicate(format: "name contains[c] cars", filterByText)

The diacritic-insensitive lookup would ignore the accents on certain letters in words, thus –

request.predicate = NSPredicate(format: "name contains[cd] citroen", filterByText)

The above would return citroen, citroën, Citroën, Citroen, and all such variations. Clever stuff!