Search code examples
swiftuipredicateswiftdata

SwiftData search bar and predicate


I'm trying to create a view using the SwiftData framework with a search bar where the user can type a registration of an airplane and dynamically update the list. But first I'm not sure if I'm doing it correctly (I can't find too many examples online) and I'm getting the error:

Generic struct 'ForEach' requires that 'Query<Aircraft, [Aircraft]>' conform to 'RandomAccessCollection

struct AirplanePick: View {
    @State var addNewPlane = false
    @Environment (\.modelContext) var mc
 
    @Binding var planePick: Aircraft?
    let dm: DataManager
    @Environment(\.presentationMode) var presentationMode

    @State var searchTerm: String = ""
    
    var filterPlanes : Query<Aircraft, [Aircraft]>{
       
        var predicate: Predicate<Aircraft>?
        
        if !searchTerm.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            predicate = .init(#Predicate { $0.registration.contains(searchTerm) })
        }
        
        return Query(filter: predicate, sort: \.registration)
    }
    var body: some View {
        
        List {
            Text("planes are \(planes.count)")
            
            ForEach(filterPlanes, id: \.self){ plane in
                             
                
                Button {
                    planePick = plane
                 
                    self.presentationMode.wrappedValue.dismiss()
                } label: {
                    HStack{
                        VStack(alignment: .leading, content: {
                            Text("Registration").foregroundStyle(.secondary).font(.subheadline)
                            Text(plane.registration).bold()
                        })
                        Spacer()
                        VStack(alignment: .trailing, content: {
                            Text("Type").foregroundStyle(.secondary).font(.subheadline)
                            Text(plane.type ?? "")
                                
                        })
                        
                        
                    }
                }

            }
            .onDelete { IndexSet in
                
                IndexSet.forEach { index in
                    let airplane = planes[index]
                    mc.delete(airplane)
                }
                
                
                
            }
           
           
        }.toolbar {
            NavigationLink("Add New") {
                AddNewAirplane()
            }
            
        }
        .searchable(text: $searchTerm)
        
        
        
        
    }
    

}

aircraft model is the following:


@Model
class Aircraft{
    @Attribute(.unique) var idAircraft: UUID  = UUID()
    var flight: Flight?
    var registration: String
    var category: String
    var type: String?
    var maxWeigth: String?
    var maker: String?
    var model: String?
    var classOF: String?
    var oper: String?
    var owner: String?
    var msn: String?
    var note: String?
    var aerobatic:Int?
    var engineType:String?
    var complex: Int?
    var efis: Int?
    var hightPerf: Int?
    var military: Int?
    var pressurize:Int?
    var radialEngine:Int?
    var tailwheel: Int?
    var turbocharger: Int?
    var ambphibian: Int?
    var retractGear: Int?
    var year: String?
    
    
    init(idAircraft: UUID, registration: String, category: String, type: String? = nil, maxWeigth: String? = nil, maker: String? = nil, model: String? = nil, classOF: String? = nil, oper: String? = nil, owner: String? = nil, msn: String? = nil, note: String? = nil, aerobatic: Int? = nil, engineType: String? = nil, complex: Int? = nil, efis: Int? = nil, hightPerf: Int? = nil, military: Int? = nil, pressurize: Int? = nil, radialEngine: Int? = nil, tailwheel: Int? = nil, turbocharger: Int? = nil, ambphibian: Int? = nil, retractGear: Int? = nil, year: String? = nil) {
        self.idAircraft = idAircraft
        self.registration = registration
        self.category = category
        self.type = type
        self.maxWeigth = maxWeigth
        self.maker = maker
        self.model = model
        self.classOF = classOF
        self.oper = oper
        self.owner = owner
        self.msn = msn
        self.note = note
        self.aerobatic = aerobatic
        self.engineType = engineType
        self.complex = complex
        self.efis = efis
        self.hightPerf = hightPerf
        self.military = military
        self.pressurize = pressurize
        self.radialEngine = radialEngine
        self.tailwheel = tailwheel
        self.turbocharger = turbocharger
        self.ambphibian = ambphibian
        self.retractGear = retractGear
        self.year = year
    }   
}

Solution

  • My first suggestion is not to use Query like this since it will mean a lot of database access so unless you have a very large number of persisted objects it is probably better to filter in memory

    @Query(sort: \.registration) var objects: [AirCraft]
    //...
    var filtered: [Aircraft] {
        guard searchText.isEmpty == false else { return objects }
    
        return objects.filter { $0.registration.contains(searchTerm)}
    }
    

    If you do want to filter using a predicate you should create a new view and pass the predicate to that view every time it changes.

    @State private var predicate: Predicate<AirCraft> = .true
    
    var body: some View {
    //...
    
           AirCraftListView(predicate: predicate)
        }
        .searchable(text: $searchTerm)
        .onChange(of: searchTerm) { old, new in
            guard new.isEmpty == false, old != new else { return }
            predicate = #Predicate<AirCraft> {
                $0.registration.contains(new)
            }
        }
      
    

    And then in the list view

    struct AirCraftListView: View {
        @Query(sort: \.registration) var objects: [AirCraft]
    
        init(predicate: Predicate< AirCraft >) {
            _objects = Query(filter: predicate)
        }