Hi everyone, 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 Word
s (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.