swiftgenericscastingswiftdata

Can't infer SwiftData model contains property name:String


In my application I have defined the following Protocol, which is intended to make all SwiftData models that inherit the NameFilter Protocol to define var name: String exits.

@Model class Session: NameFilter { var name: String var x: .... var y: ... }

import Foundation

protocol NameFilter{
    associatedtype String
    
    var name: String {get set}
    
}

My Session DataModel class

import Foundation
import SwiftData

@Model final class WCSession: NameFilter {
    var id: String = UUID().uuidString
    var name: String = ""  //i.e. Spring 2024
    var number: String = "" // 133
    var startDate: Date = Date.distantPast // Jun 1
    var isCurrent: Bool = false  // True

    
    init(name: String, number: String, startDate: Date, isCurrent: Bool) {
        self.name = name
        self.number = number
        self.startDate = startDate
        self.isCurrent = isCurrent
    }
}

My Generic View


import SwiftUI
import Observation
import SwiftData

struct NavigationStackToolbar<T>: View {
    
    // Bring in my Observable ApplicationData
    @Environment(ApplicationData.self) var appData
    
    // Bring in my modelContext defined as dbContext
    @Environment(\.modelContext) var dbContext
    
    // Setup my DataQuery of type T
    @Query var listItems: [T]
    
    // passed in dataModel from Caller
    @State var dataModel: String
    
    // Our Search Query
    @State var searchQuery = ""
    
    // Define NameFilter Protocols
    @State var name: String = ""
    
    // Setup filter -> Pass in Query Filter or Build here with helpers
    
    var filteredItems: [T] {
        if searchQuery.isEmpty {
            return listItems
        }
        
// Error on line below : Generic parameter 'ElementOfResult' could not be inferred
// ***********************************************VVVVVVVV *********
        let filteredItems = listItems.compactMap { item in
            
            let nameContainsQuery = (item.name as String).range(of: searchQuery, options: .caseInsensitive) != nil
            return(nameContainsQuery) ? listItems : nil
        }
        
        return filteredItems
    }
    
    var body: some View {
        NavigationStack(path: Bindable(appData).viewPath) {
            List(filteredItems) { item in
                HStack {
                    CellSession(item: item)
                }
            }
        }
    }
}

#Preview {
      
    var filterSessions: [WCSession] {
        let filterSessions = listSessions.compactMap { session in
            let nameContainsQuery = session.name.range(of: searchQuery, options: .caseInsensitive) != nil
            return (nameContainsQuery) ? session : nil
        }
    }
    
    NavigationStackToolbar<Any>(dataModel: "Session", searchQuery: filterSessions)
        .environment(ApplicationData())
}

I realize that I need to help the compile know the type by type casting the value.

I have tried the following but can make it work

let nameContainsQuery = (item.name as String).range(of: searchQuery, options: .caseInsensitive) != nil

//. item.name is indicated with the error --> X Value of type 'T' has no member 'name'

The idea in this design is to simplify the view so that is is broken down into parts that can be quickly type checked. I get error stating Compiler ran out of time type checking view. Please break it down into small components ...

I want to create a prototype Model that has a generic Filter on the Model Properity Name which is of type string. Any SwiftData Model that inherits NameFilter would be eligable to filter that view by Name Generically

I am thinking now that code:

struct NavigationStackToolbar<T> : View 

Informs the complile that we are sending Type T, inherits NameFitler, but the error indicates that Type T has no memeber Name...

Is there away to write code so that we can force the type cast?

found this link: https://forums.swift.org/t/when-generic-parameter-could-not-be-inferred/18731

This links, discusses Type Alias. Not understanding completly yet.


Solution

  • There are some different issues that needs to be fixed, first the associated type for the protocol makes no sense (unless you really want it to be generic with a generic type named String)

    So the protocol becomes

    protocol NameFilter{
        var name: String {get set}
    }
    

    Then your view can't be generic for any type so we need to restrain what T can be. It should of course conform to NameFilter but it should also conform to PersistentModel because that is what @Query expects.

    So add a type alias that combines the two

    typealias ModelNameFilter = NameFilter & PersistentModel
    

    And then use that in the view declaration

    struct NavigationStackToolbar<T: ModelNameFilter>: View
    

    Now the code in the computed property will compile and run but I believe the code can be simplified to

    var filteredItems: [T] {
        if searchQuery.isEmpty {
            return listItems
        }
    
        return listItems.filter { item in
            item.name.localizedCaseInsensitiveContains(searchQuery)
        }
    }