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: