SwiftData is a powerful framework that provides a convenient way to manage and interact with data in Swift applications. One of the key features of SwiftData is the ability to use predicates for data filtering and searching. SwiftData predicate allows you to define logical conditions to retrieve specific data that match your criteria. In this blog post, I will show you how to use predicates with SwiftData in your SwiftUI applications.
⬇️ Download the project from GitHub https://github.com/gahntpo/SnippetBox-SwiftData
I used Xcode 15 to write the code for this post. If you want to learn about the basics in SwiftData check out this post.
Demo Project: SnippetBox
I’ll use a demo project called ‘SnippetBox’ – a tool that helps programmers save and organize frequently used code snippets – to bring the concepts to life.
I added different property types for Snippet like Date, UUID, and Bool. Some more advanced use cases are images and enums. Each Snippet can belong to one folder and a folder can have many snippets, which is represented by a one-to-many relationship. On the other hand, each snippet can have many tags, and a tag can be added to many snippets, which is a many-to-many relationship. In the following graph, you can see the schema.
If you want to read how I set up the schema, you can read this blog post where I explain all property types and relationships
How to search with SwiftData predicates in a SwiftUI View
Predicates are logical conditions that evaluate a Boolean value (true or false). They are commonly used for filtering and sorting data based on specific criteria. With SwiftData, you can use predicates to define the conditions for fetching and manipulating data from your data store.
To leverage predicates in your SwiftData queries, you need to use the @Query property wrapper. The @Query property wrapper allows you to specify the filter and sort descriptors for your data retrieval. Let’s take a closer look at how to use predicates with SwiftData queries:
import SwiftData
import SwiftUI
struct ContentView: View {
@Query(filter: #Predicate<Snippet> { $0.title.contains("test") },
sort: [SortDescriptor(\Snippet.creationDate)] )
var snippets: [Snippet]
var body: some View {
List {
Section("All Snippets") {
ForEach(snippets) { snippet in
SnippetRow(snippet: snippet)
}
}
}
}
}
In the above code example, I defined a `ContentView` struct that includes an @Query property wrapper. Within the @Query property wrapper, I pass the filter and sort descriptors as arguments. Here, the filter descriptor uses the predicate macro, #Predicate, to define the filtering condition. In this example, we filter snippets based on whether their titles contain the string “test”. The `sort` argument specifies the sorting order for the retrieved data, using the `SortDescriptor` struct.
Constructing Predicate Conditions
Predicates in SwiftData allow for powerful and flexible conditions. You can use a variety of operations and expressions within the predicate closure to construct your filtering conditions. Some commonly used operations and expressions include arithmetic operations, comparisons, boolean logic, and sequence operations.
For instance, let’s say we want to filter snippets based on a specific date range:
@Query(filter: #Predicate<Snippet> { $0.creationDate > startDate && $0.creationDate < endDate })
var snippets: [Snippet]
In the above example, we use the `>` and `<` comparison operators to filter snippets that have a creation date falling between `startDate` and `endDate`.
Example: How to filter and only show favorite items
You can filter your query with a predicate. For example, the Snippet type has a `isFavorite` boolean property. We can filter the query and only show a section with only the favorite snippets. In the following example, I am showing 2 sections in a list with all snippets and a favorite section:
import SwiftUI
import SwiftData
struct ContentView: View {
@Query(sort: [SortDescriptor(\Snippet.creationDate)] )
var allSnippets: [Snippet]
@Query(filter: #Predicate<Snippet> { $0.isFavorite },
sort: [SortDescriptor(\.creationDate)] )
var favoriteSnippets: [Snippet]
var body: some View {
List {
Section("All Snippets") {
ForEach(allSnippets){ snippet in
SnippetRow(snippet: snippet)
}
}
Section("Favorite Snippets") {
ForEach(favoriteSnippets){ snippet in
SnippetRow(snippet: snippet)
}
}
}
}
}
Example: How to filter for non-nil properties
Snippet has an ´image´ property that is of type optional Data. If I would want to filter the snippet list and only show snippets with an image attached, I would use the following query:
@Query(filter: #Predicate<Snippet> { $0.image != nil },
sort: [SortDescriptor(\Snippet.creationDate)] )
var imageSnippets: [Snippet]
Complex Predicate Expressions
Predicates can be further enhanced by combining multiple conditions and nesting expressions. This allows you to build more complex queries with precise filtering and sorting requirements. SwiftData supports the use of closures, enabling you to create nested expressions and combine them within the predicate.
@Query(filter: #Predicate<Snippet> { snippet in
snippet.title.contains("test") &&
(snippet.creationDate > startDate || snippet.modifiedDate > startDate)
})
In the above example, we filter snippets based on the condition that their titles contain “test” and either the creation date or modified date is greater than `startDate`.
In the following is a predicate where I search for snippets that contain any tags that have a name equal to “funny tag name”:
let predicate = #Predicate<Snippet> { snippet in
snippet.tags?.contains {
$0.name == "funny tag name"
} == true
}
This is a complex nested expression.
Searching for Conditions with Relationships
Sometimes you want to search for objects with a certain relationship. For example, I want to query all snippets belonging to a specific folder. The following example uses the intializer to updated the query to the selected folder´s snippets:
import SwiftUI
import SwiftData
struct SnippetListView: View {
let folder: Folder
@Query(sort: \Snippet.creationDate, order: .reverse)
var snippets: [Snippet]
init(for folder: Folder,) {
let id = folder.uuid // need to extract this first
self._snippets = Query(filter: #Predicate {
$0.folder?.uuid == id
}, sort: \.creationDate)
self.folder = folder
}
var body: some View {
List(snippets) { snippet in
SnippetRow(snippet: snippet)
}
}
}
How to use predicates with optional values
Note that when working with optionals the predicates often don’t work when using optional chaining. You need to return a non-optional boolean. For example, let’s say I want to filter all snippets that are in folders that contain the string “new folders”. The following statement would return an optional boolean:
$0.folder?.name.contains("new folder")
whereas the following code, although looking slightly overcomplicated returns a boolean, and the SwiftData query will work:
@Query(filter: #Predicate<Snippet> {
$0.folder?.name.contains("new folder") == true
},sort: [SortDescriptor(\.creationDate)] )
var newFolderSnippets: [Snippet]
Case Insensitive Filtering
This is another example, where I search for snippets that are in folders with the title “new folders”.
@Query(filter: #Predicate<Snippet> {
$0.folder?.name == "new folder"
},sort: [SortDescriptor(\.creationDate)] )
var newFolderSnippets: [Snippet]
I would like to show you how to support case insensitive filtering. However, this is not supported with predicate currently:
@Query(filter: #Predicate<Snippet> {
$0.title.localizedCaseInsensitiveContains("tEst") // does not compile
}, sort: [SortDescriptor(\Snippet.creationDate)] )
var searchTermSnippets: [Snippet]
Example: Searching for snippets that are not attached to a folder
You can also check if certain properties are nil or not. For example, I can search for snippets that are not included in any folder with the following predicate:
@Query(filter: #Predicate<Snippet> {
$0.folder == nil
},sort: [SortDescriptor(\Snippet.creationDate)] )
var nofolderSnippets: [Snippet]
Example: Filtering to show folders that are not empty
Another example is to search for objects that have relationships set or not. For example, I want to show all folders that are empty:
@Query(filter: #Predicate<Folder> { !$0.snippets.isEmpty },
sort: [SortDescriptor(\Folder.creationDate)] )
var snippetFolders: [Folder]
@Query(filter: #Predicate<Folder> { $0.snippets.count >= 2 },
sort: [SortDescriptor(\Folder.creationDate)] )
var twoormoresnippetFolders: [Folder]
If you use an optional relationship, you would need to handle the non optional return like:
@Query(filter: #Predicate<Folder> { $0.snippets?.isEmpty == false },
sort: [SortDescriptor(\Folder.creationDate)] )
var snippetFolders: [Folder]
Example: How to fetch folders that contain favorite snippets
You can use the contains and allSatisfy sequence function to filter to-many relationships. For example, I can filter all folders that have any favorite snippets:
@Query(filter: #Predicate<Folder> {
$0.snippets.contains {
$0.isFavorite
}
},sort: [SortDescriptor(\Folder.creationDate)] )
var someFavoriteSnippetFolders: [Folder]
or filter for folders that have only favorite snippets included and are not empty:
@Query(filter: #Predicate<Folder> {
$0.snippets.allSatisfy {
$0.isFavorite
} && !$0.snippets.isEmpty
}, sort: [SortDescriptor(\Folder.creationDate)] )
var allFavoriteSnippetFolders: [Folder]
Similarly, I would handle optional relationships like:
@Query(filter: #Predicate<Folder> {
$0.snippets?.contains {
$0.isFavorite
} == true // make this a boolean return value
},sort: [SortDescriptor(\Folder.creationDate)] )
var someFavoriteSnippetFolders: [Folder]
How to dynamically change filtering and sorting in SwiftData
Unfortunately, you can not change the @Query properties in SwiftUI. But I will show a workaround that uses the view initializer to set the query properties. As an example, I implemented a search and sorting feature for the tag list in the Snippet demo app:
The Tag list view shows the search text field and sort picker:
struct AddTagToSnippetsView: View {
let snippet: Snippet
@State private var searchTerm: String = ""
@State private var selectedTags = Set<Tag>()
@State private var tagSorting = TagSorting.aToZ
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
TagListView(searchTerm: searchTerm,
sorting: tagSorting,
selectedTags: $selectedTags,
snippet: snippet)
.padding(.horizontal)
.navigationTitle("Add tags to \(snippet.title)")
.navigationBarTitleDisplayMode(.inline)
.searchable(text: $searchTerm)
.toolbar(content: {
Menu {
Picker(selection: $tagSorting.animation()) {
ForEach(TagSorting.allCases) { tag in
Text(tag.title)
}
} label: {
Text("Sort Tags by")
}
} label: {
Label("Sorting", systemImage: "slider.horizontal.3")
}
})
}
.frame(minWidth: 300, minHeight: 300)
}
}
I defined an enum TagSorting that I am using in the sorting picker:
import Foundation
enum TagSorting: String, Identifiable, CaseIterable {
case aToZ
case ztoA
case latest
case oldest
var title: String {
switch self {
case .aToZ:
return "A to Z"
case .ztoA:
return "Z to A"
case .latest:
return "Latest"
case .oldest:
return "Oldest"
}
}
switch self {
case .aToZ:
SortDescriptor(\Tag.name, order: .forward)
case .ztoA:
SortDescriptor(\Tag.name, order: .reverse)
case .latest:
SortDescriptor(\Tag.creationDate, order: .reverse)
case .oldest:
SortDescriptor(\Tag.creationDate, order: .forward)
}
}
var id: Self { return self }
}
I also added a computed property sortDescriptor that generates a SortDescriptor instance for the chosen sorting. SortDescriptor is a new type introduced with iOS 17 that works together with SwiftData queries.
I am passing all parameters to the TagListView that I need for updating the query:
struct AddTagToSnippetsView: View {
let snippet: Snippet
@State private var searchTerm: String = ""
@State private var selectedTags = Set<Tag>()
@State private var tagSorting = TagSorting.aToZ
var body: some View {
NavigationStack {
TagListView(searchTerm: searchTerm,
sorting: tagSorting,
selectedTags: $selectedTags,
snippet: snippet)
...
}
}
}
In the TagListView initializer, I am updating the query:
import SwiftUI
import SwiftData
struct TagListView: View {
@Query(sort: \Tag.name, order: .forward)
private var tags: [Tag]
init(searchTerm: String,
sorting: TagSorting) {
if searchTerm.count > 0 {
self._tags = Query(filter: #Predicate<Tag> {
$0.name.contains(searchTerm)
}, sort: [sorting.sortDescriptor],
animation: .easeInOut)
} else {
self._tags = Query(sort: [sorting.sortDescriptor],
animation: .easeInOut)
}
}
var body: some View {
List(selection: $selectedTags) {
ForEach(tags) { tag in
Text(tag.name)
}
}
}
}
@Query has multiple initialisers where you can pass predicates for filtering and SortDesciptors. If I have a search text, I am using a predicate to search for snippets with titles containing this search term:
let predicate = #Predicate<Tag> { $0.name.contains(searchTerm) }
How can I implement a sectioned fetch in SwiftData
SwifData query does not support sectioned fetches like ´@SectionedFetchRequest´ with Core Data. I think that this will likely be added in the next iteration with iOS 18.
As a workaround, you can generate multiple queries that will each represent a section. For example, one section for favorite snippets and one for the rest:
@Query(filter: #Predicate<Snippet> { $0.isFavorite },
sort: [SortDescriptor(\Snippet.creationDate)] )
private var favoriteSnippets: [Snippet]
@Query(filter: #Predicate<Snippet> { !$0.isFavorite },
sort: [SortDescriptor(\Snippet.creationDate)] )
private var nonFavoriteSnippets: [Snippet]
Conclusion
Using predicates with SwiftData queries provides a powerful mechanism for filtering and sorting data in your Swift applications. Predicates allow you to define precise conditions for retrieving the desired data from your data store, enabling efficient data management. By utilizing predicates, you can simplify and streamline the process of retrieving and manipulating. Unfortunately, querying with SwiftData is currently very buggy and limited with relationships.
Further Reading:
Hi theгe i am kavin, іts my first occasion to commеnting аnyplace,
when i reаd thіs аrticle i thought i could also create сomment due to this sensible piece of writing.
Hello Karin.
Thank you for this. I have been looking very hard to find a solution for the favorites sort problem using @Query predicates. I have downloaded your code. However I am sorry to say it’s so complicated … why do you need to have 18 folders and more than thirty files.
I wish I could find a SIMPLE solution … keep in mind that we are not advanced that is why we are looking for answers. Therefore having one or two complete examples is better than thousands of variables that need to be read with interconnected files.
I was able to test your app (linked above) on my phone it does work on my phone but the whole thing is so complicated to understand as a codebase.
Why 30 files …I only need 2 files
1-Model
2-ContentView ( where the @Query search is conducted on top )
—>A button one can press to make the item a ‘favorite’, ideally on the ContentView itself.
—>A another button on the toolbar that enables the user to selected it to show her the favorites. You know how apps like instagram have little hearts the user presses to show her favorites at the bottom of the view and then on each item that can be selected as a ‘favorite’. I have the ‘favorite’ variable under the @Model as var isFavorite = false boolean. I have the little hearts that work they are just missing the @Query predicate. The tags sorting work, the name/date sorting works.
I have the UI perfected I just need the code to make the @Query usable. It’s the only thing I am missing in order to finish my app, all the other queries work. Is there a way to do this WITHOUT resorting to 30 different files and 18 folders? I am at my wits end. It doesn’t have to be complicated really. Thank you.
Ahaa, its niⅽe conversation regаrding this piece of writing at this
place ɑt this weblog, I have read all that, so now me ɑlѕo
commenting at this place.
I enjоy, lead to I found just what I was taking a look for.
Υoᥙ have endeԁ my 4 day lengthy hᥙnt! God Bless you man. Havе
a great day. Bye
Whаt’s ᥙp, just wanted to mentіon, Ι liked this post.
It was inspiring. Keep on posting!
We stumbled ߋver here diffегent web page and thought I should
cheϲk things out. I ⅼike what I see so now i am following yoս.
Look forward to looking into your web page yеt again.
I constantlʏ spent my half an hour to read this website’s posts everyday along witһ a ⅽup of coffee.
Thank You very much for one of the best and complete in-depth articles I could find concerning SwiftData.
I was interested in how You handle to-many relationships with Optionals, that you need for iCloud syncing with CloudKit.
The code of Chapter 4.
snippet.tag?.contains {} is not working and crashes with the message: “to-many key not allowed here”
let predicate = #Predicate { snippet in
snippet.tags?.contains {
$0.name == “funny tag name”
} == true
}
In Your Github code You changed
@Relationship(inverse: \Tag.snippets)
var tags: [Tag]?
to
@Relationship(inverse: \Tag.snippets)
var tags: [Tag]
without Optional Arrays. This does not crash, but will not work with iCloud.
This most probably means, that you can’t presently use SwiftData to sync with iCloud.
Yes. Currently, SwiftData has issues with handling optional to-many relationships.
I was hoping they would fix this at some point
Great article, but this is quite a record scratch. Any thoughts on workarounds? It’s such a common scenario to query against to-many relationships, while supporting CloudKit. Are we really stuck with moving to Core Data for this?
I feel you, this is a very limiting problem. I would really like them to work on CloudKit.
Nice overview of SwiftData queries, however when I try the isEmpty or count filter predicates on an optional to-many relationship, it crashes with `to-many keys not allowed here`. This affects for example this one:
“`swift
@Query(filter: #Predicate { $0.snippets?.isEmpty == false },
sort: [SortDescriptor(\Folder.creationDate)] )
var snippetFolders: [Folder]
“`
Thanks so much for sharing this awesome info! I am looking forward to see more postsby you!
I much appreciate this non-trivial example that’s not just a one-dimensional demo of SwiftData. Here’s something I still don’t quite understand:
Snippet has a (optional) pointer to a folder, which is a traditional approach in a relational database, i.e. the Snippet can (but doesn’t have to) belong to a folder. I get that you @Query to get a list of Snippets that match what you want, including, say, membership in a Folder as dictated by the pointer to said Folder.
So then why does the Folder have an array of Snippets? You pass the Folder to the Snippet viewer, the Snippet viewer should do the @Query to find Snippets that belong to Folder, and everyone’s happy. But it seems that the Snippet viewer could in theory just use the list that’s passed to it inside Folder, which seems counter to how a relational db works. Does the Folder /have/ to have a list of Snippets for SwiftData to work?
Нello, I enjoy reading through your artіcle post. I wanted to
write a little comment to ѕupport you.
Very nice artiϲle, exactly what I needed.
yοu are actᥙаlly a good webmaster. Tһe site loading speed iѕ amazing.
It sort of feels that you arе d᧐ing any unique tricқ.
Morеover, The contents are masterwork. you’ve done a excellent job in this matter!
always i useԀ to read smaller articles or reviews that
also clear their motive, and that is also happening with thiѕ
article which I ɑm reading now.
Ӏ’m very happy to find this gгeat ѕite. I want to to thank you for ones time
for this fantastic reaԀ!! I definitely liked evеry little
bit of it and I have you Ƅookmarked to look at new stuff in your web site.
Usеful information. Lucky me I discovered your wеbsite accidentally, and I’m
ѕurpriseԀ why this twist of fate didn’t took place
in advance! I bоokmarked it.
Your style іs verʏ ᥙniqսe cߋmpared to otheг folкs
I’ѵe read stuff from. Thanks for posting when you һave
the opρortunity, Guess I will just book mark this page.
Ѕimply desire to say your article is as astonishing.
The ϲlearness for your submit is just great and i can assume you are knowledgeable on this subject.
Fine along with your permission allow me to grasp your feed to stay
up to date with impending post. Ƭhank yoᥙ 1,000,000 and please keep up the enjoyable wߋrk.
Thiѕ website definitely һas all the information I needed about this subject and didn’t know
who to ask.
Thanks vеry nice blog!
I’m really enjoying thе design and layout of your blog.
It’s а very easy on the eyes which makes it mᥙch more pleasant for me to come here and visit more
often. Did you hire out a designer to create your theme?
Outstаnding work!
I blog qᥙite оften and I truly thank you for your information. This grеat article hɑs
really peaked my interest. I am going to bookmark your website and keep chеckіng for new informati᧐n about once a week.
I opted in f᧐г ʏour RSS feed as well.