mallow
mallow

Reputation: 2866

dynamic filters (predicate) in SwiftUI

I am writing an app for iOS using SwiftUI and CoreData. I am trying to solve one problem for a few days now. How to make dynamic filters using dynamically changing predicate in SwiftUI based on user input?

I have followed this tutorial to learn about dynamic filters and CoreData: https://www.hackingwithswift.com/quick-start/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui

After few small changes I have the following code. ContentView.swift:

import SwiftUI

struct ContentView: View {

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

    var body: some View {

        VStack {
            FilteredList(predicate: lastNameFilter)

            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"
            }
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

FilteredList.swift:

import CoreData
import SwiftUI

struct FilteredList: View {

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

    var body: some View {
        List(singers, id: \.self) { singer in
            Text("\(singer.firstName ?? "Unknown") \(singer.lastName ?? "Unknown")")
        }
    }

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

}

//struct FilteredList_Previews: PreviewProvider {
//    static var previews: some View {
//    }
//}

I also have 1 entity named Singer and this entity has 2 attributes: firstName and lastName, both of which are Strings. Above example seems to work fine in Simulator, but crashes the app when using Preview in Xcode.

I would appreciate any help, for example:

Upvotes: 2

Views: 2787

Answers (2)

malhal
malhal

Reputation: 30746

Here is another way:

import CoreData
import SwiftUI

struct FilteredList: View {

    @FetchRequest var singers: FetchedResults<Singer>

    init(lastName: String) {
        _singers = FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "lastName BEGINSWITH %@", lastName))
    }

    var body: some View {
        List(singers) { singer in
            Text("\(singer.firstName ?? "Unknown") \(singer.lastName ?? "Unknown")")
        }
    }
}

Be aware that every time the View is init and the FetchRequest is init the database is hit so you may need to lift the fetch into a superview.

Upvotes: 0

Gergely
Gergely

Reputation: 448

  1. To make the preview of ContentView work you should write something like the following:
static var previews: some View {
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    let taylor = Singer(context: context)
    taylor.firstName = "Taylor"
    taylor.lastName = "Swift"
    let ed = Singer(context: context)
    ed.firstName = "Ed"
    ed.lastName = "Sheeran"
    let adele = Singer(context: context)
    adele.firstName = "Adele"
    adele.lastName = "Adkins"
    return ContentView().environment(\.managedObjectContext, context)
}
  1. You could implement a more generic FilteredList type where you only provide the predicate for the list (optionally a sort descriptor). Example:
struct FilteredList<T: NSManagedObject, Content: View>: View {
    var fetchRequest: FetchRequest<T>
    var items: FetchedResults<T> { fetchRequest.wrappedValue }

    let content: (T) -> Content

    var body: some View {
        List(items, id: \.self) { item in
            self.content(item)
        }
    }

    init(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor] = [], @ViewBuilder content: @escaping (T) -> Content) {
        fetchRequest = FetchRequest<T>(entity: T.entity(), sortDescriptors: sortDescriptors, predicate: predicate)
        self.content = content
    }
}

Use this new FilteredList type with @State private var predicate: NSPredicate? instead of @State var lastNameFilter = "A". When new filtering is needed just set this private @State property to the new predicate and the list will be updated accordingly.

The concrete usage would be:

FilteredList(predicate: predicate) { (singer: Singer) in
    Text("\(singer.firstName ?? "") \(singer.lastName ?? "")")
}

Preview of the FilteredList:

static var previews: some View {
    FilteredList(predicate: nil) { (singer: Singer) in
        Text("\(singer.firstName ?? "") \(singer.lastName ?? "")")
    }.environment(\.managedObjectContext, (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext)
}

Upvotes: 3

Related Questions