ios – SwiftData Predicate changing at user input?


I’ve been fighting with SwiftData for the past few days without reaching an understanding. I have to say I’m just a beginner so I may have made mistakes somewhere else too, but still I don’t understand.

So, what I’m trying to do is to have a list of Words (a class of mine), which are stored in SwiftData, filtered depending on the category the user chooses. It appears SwiftData has other ideas though.

For organizing the code I took inspiration from Apple’s sample code.

Category model

Let’s start with the Category model (which represents the category a word may belong to). ColorComponents is a very simple Codable struct I wrote to store a color, not important.

import Foundation
import SwiftData

@Model
class Category: Codable, Equatable {
    enum CodingKeys: CodingKey {
        case name, primaryColor, secondaryColor
    }
    
    @Attribute(.unique) let name: String
    let primaryColor: ColorComponents
    let secondaryColor: ColorComponents
    
    init(name: String, primaryColor: ColorComponents, secondaryColor: ColorComponents) {
        self.name = name
        self.primaryColor = primaryColor
        self.secondaryColor = secondaryColor
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.primaryColor = try container.decode(ColorComponents.self, forKey: .primaryColor)
        self.secondaryColor = try container.decode(ColorComponents.self, forKey: .secondaryColor)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.name, forKey: .name)
        try container.encode(self.primaryColor, forKey: .primaryColor)
        try container.encode(self.secondaryColor, forKey: .secondaryColor)
    }
    
    static func ==(lhs: Category, rhs: Category) -> Bool {
        lhs.name == rhs.name
    }
    
    static let example = Category(name: "General", primaryColor: ColorComponents(color: .mint), secondaryColor: ColorComponents(color: .blue))
}

Word model

Then, the Word model. Now, this contains a static method to return a predicate. Apple’s sample code suggests this and is perhaps the only way to have a predicate changing together with its input data.

import Foundation
import SwiftData

@Model
class Word {
    let term: String
    let learntOn: Date
    var notes: String
    @Relationship var category: Category?
    
    var categoryName: String {
        category?.name ?? "No category"
    }
    
    init(term: String, learntOn: Date, notes: String = "", category: Category? = nil) {
        self.term = term
        self.learntOn = learntOn
        self.notes = notes
        self.category = category
    }
    
    static func predicate(category: Category?) -> Predicate<Word> {
        return #Predicate<Word> { word in
            // this expression is what I would like to have, but it throws an error at runtime
            category == nil || word.category == category
        }
    }
    
    static let example = Word(term: "Swift", learntOn: .now, notes: "A swift testing word.")
}

These are the two models I have. In the main view I create the model container using .modelContainer(for: Word.self).

SwiftUI View

I then have the view where the query is being made. According to Apple, given that the category is passed to the initializer itself, this way of doing things ensures that the query is updated at every category change (that ideally I’d like for the user to be able to select at any time).

import SwiftData
import SwiftUI

struct WordsCardsListView: View {
    let category: Category?
    @Query private var words: [Word]
    
    init(category: Category? = .example) {
        self.category = category
        
        let predicate = Word.predicate(category: category!)    // force unwrapping just for testing, of course
        let sortDescriptors = [
            SortDescriptor(\Word.learntOn, order: .reverse)
        ]
        _words = Query(filter: predicate, sort: sortDescriptors)
    }
    
    var body: some View {
        List {
            // other views
            
            ForEach(words) { word in
                WordCardView(word: word)
                    .listRowSeparator(.hidden)
            }
        }
        .listStyle(.plain)
    }
}

The errors I get

I did try every combination possible, I believe, but I always get a SwiftData.SwiftDataError._Error.unsupportedPredicate error at runtime (or sometimes the predicate won’t even compile). From what I can gather the predicate does not support comparing objects (perhaps, it fails every time I try to compare a Category or even a Word) and it also fails when trying to access word.category?.name, either with optional chaining or force unwrapping (given that the category’s name is unique I would have been ok with that too). I do know that predicates are somewhat limited in what they can accept as expressions, but I don’t understand why Apple implementation works and mine does not, since I believe there are not significant differences.

I do know that the easiest solution would be to just query for all words and then filter them afterwards (and it’s probably what I will end up doing), but it puzzles me that such a simple idea (a filter that updates live) is not so easy to obtain with SwiftData.

Anyway, I thank anyone that read up to this point and that will take the time to answer.

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img