Easy Predicate for SwiftData
I recently worked on a SwiftData app where I needed to filter data in a Query while loading views. The key difference across my views was that they all used the same property as a filter:
var status: Status
The Problem with Enums in SwiftData Filters
SwiftData currently has limitations when using enums in the filter
property. I won't go into full detail, as this topic has already been extensively covered:
However, one crucial thing to note is that SwiftData filtering requires primitive types, such as String
or Int
.
Let's take a look at an example:
@Model
final class Pet {
var status: Status
init(status: Status) {
self.status = status
}
}
Now, if we try to use status
as an enum in the filter
, we get an error:
Member access without an explicit base is not supported in this predicate.
struct ActivePetList: View {
@Query private var pets: [Pet]
init() {
self._pets = Query(filter: #Predicate { $0.status == .active })
}
var body: some View {
// .. your view
}
}
The Workaround: Using a Primitive Type
To bypass this issue, we can store the raw primitive type (String
) and create a computed property (getter
and setter
) to handle the conversion:
@Model
final class Pet {
var statusRaw: String
var status: Status {
get { Status(rawValue: statusRaw) ?? .active }
set { statusRaw = newValue.rawValue }
}
init(status: Status) {
self.statusRaw = status.rawValue
}
}
This allows us to use the statusRaw
property in the Query
:
struct ActivePetList: View {
@Query private var pets: [Pet]
init() {
self._pets = Query(filter: #Predicate { $0.statusRaw == "active" })
}
var body: some View {
// .. your view
}
}
The Issue with Strings in Filters
While this approach works, using raw strings introduces risk - especially when multiple screens require filtering. Consider a scenario with statuses like .active
, .inactive
, and .pendingDeletion
. A simple typo in a string could lead to hours of debugging.
Wouldn't it be safer to do this instead?
init() {
self._pets = Query(filter: #Predicate { $0.statusRaw == Status.active.rawValue })
}
Surprisingly, this results in an error:
Key path cannot refer to enum case 'active'
A Cleaner Approach: Using a Predicate Extension
To avoid these pitfalls, a neat trick is to extend Predicate
and define static variables for common filters:
extension Predicate {
static var activePets: Predicate<Pet> {
#Predicate<Pet> { $0.status == .active }
}
static var inActivePets: Predicate<Pet> {
#Predicate<Pet> { $0.status == .inActive }
}
}
But we can improve this further by making it a static function:
extension Predicate {
static func pets(with status: Status) -> Predicate<Pet> {
#Predicate<Pet> { $0.statusRaw == status.rawValue }
}
}
Now, using it in a query is much cleaner and safer:
init() {
self._pets = Query(filter: .pets(with: .active))
// self._pets = Query(filter: .pets(with: .inActive))
// self._pets = Query(filter: .pets(with: .pendingDeletion))
}
Conclusion
By leveraging an extension on Predicate
, we avoid the pitfalls of raw strings while keeping our filters type-safe and easy to use. This method makes filtering SwiftData queries more robust and less error-prone.
If you found this article helpful, you can keep the ideas flowing by supporting me. Buy me a coffee or check out my apps to help me create more content like this!
Coffee Check out my apps