How to Query and Filter Data in SwiftData with Predicate

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.

snippet sample project for swiftdata
SwiftData demo project with folders and snippet collections

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.

SwiftData model example with relationships

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.

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

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)
                }
            }
        }
    }
}
Filter favorites in SwiftData example

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]
Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

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:

example app that shows how to filter and sort data with SwiftData predicate in SwiftUI
The SnippetBox demo project allows you to add tags to snippets. You can search and filter existing tags in the tag list view.

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)
    }
} 
Using searchable with SwiftData queries

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:

27 thoughts on “How to Query and Filter Data in SwiftData with Predicate”

  1. 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.

    Reply
  2. 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.

    Reply
  3. 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.

    Reply
  4. 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.

    Reply
  5. 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.

    Reply
  6. 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]
    “`

    Reply
  7. 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?

    Reply
  8. 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!

    Reply
  9. 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.

    Reply
  10. Ѕ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.

    Reply
  11. 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!

    Reply
  12. 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.

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form