This story on HackerNoon has a decentralized backup on Sia.
Transaction ID: xv_GdiB-EJ-EPCCEepfLE9zc8xGSLJcEHg4C_HeWdac
Cover

Swift’s #Predicate Explained: How Type-Safe Filtering Works in SwiftData

Written by @herlandro | Published on 2025/12/11

TL;DR
Swift’s new #Predicate macro turns query filtering into a type-safe, compile-time-checked process for SwiftData, but it requires comparing scalar identifiers—not whole objects—to generate valid database expressions. This guide explains how it works, why object comparisons fail, and the correct pattern for writing robust SwiftData filters.

A "#Predicate" is a boolean condition used to select or filter records. It answers "does this object meet the criteria?" and returns true or false.

In databases/ORMs, the predicate is sent to the persistence engine, which returns only the records that satisfy the condition.


In the Apple ecosystem, we have:

  • NSPredicate: The classic API (Core Data, Foundation) based on formatted strings, like NSPredicate(format: "age >= %d", 18). It's flexible, but less safe (errors only appear at runtime).
  • #Predicate: A modern, type-safe DSL (SwiftData/SwiftUI). You write conditions with Swift code, and the macro generates a compile-time safe boolean expression.


Usage in SwiftData/SwiftUI

@Query(filter: #Predicate { ... }), where $0 represents a record of type Model.


You compare fields with captured values:

// Example with strings:
#Predicate<CompanyAndLocation> {
  $0.company.name == companyName &&
  $0.location.location == locationName
}

// Example by identity:
#Predicate<CompanyAndLocation> {
  $0.company.id == companyId &&
  $0.location.id == locationId
}


This allows the filter to run in the store, returning only items that match the criteria.


When should you use #Predicate?

Filter a persisted collection via @Query or FetchDescriptor to bring from the database only what matters. It replaces NSPredicate when you want type safety and direct integration with SwiftData.

What are the advantages of #Predicate?

  • Type-checked at compile-time.
  • Integrates with @Query and FetchDescriptor, reducing formatting errors.
  • Improves completions and refactoring (if you rename a field, the compiler helps).

Watch out for some small pitfalls!

  • Comparing "key path to key path" instead of "field to value" causes type errors in the DSL.
  • Referencing self or properties of @Model inside the block without capturing simple values can break macro expansion.
  • Comparing relationships by the entire object may fail depending on the SDK version; comparing by id or unique attributes is more robust.


Code Examples

Let me show you the complete example with all the files:

// PredicateApp.swift

import SwiftUI
import SwiftData

@main
struct PredicateApp: App {
  var body: some Scene {
    WindowGroup {
      DataView(
        company: Company(name: "Company 1", type: 1),
        location: Location(location: "Location 1", value: 100)
      )
    }
  }
}


// CompanyAndLocation.swift

import SwiftData

@Model
class Company {
  #Unique<Company>([.name])
  var name: String
  var type: Int

  init(name: String, type: Int) {
    self.name = name
    self.type = type
  }
}

@Model
class Location {
  #Unique<Location>([.location])
  var location: String
  var value: Int

  init(location: String, value: Int) {
    self.location = location
    self.value = value
  }
}

@Model
class CompanyAndLocation {
  #Unique<CompanyAndLocation>([.company, .location])
  var company: Company
  var location: Location

  init(company: Company, location: Location) {
    self.company = company
    self.location = location
  }
}


// DataView.swift

import SwiftUI
import SwiftData

struct DataView: View {

  @Environment(\.modelContext) var modelContext
  var company: Company
  var location: Location
  
  @Query
  var companyAndLocation: [CompanyAndLocation]
  var body: some View {
    Text("Hello World!")
  }

  init(company: Company, location: Location) {
    self.company = company
    self.location = location
    let predicate = #Predicate<CompanyAndLocation> 
    {
      $0.company == self.company && $0.location == self.location
    }

    _companyAndLocation = Query(filter: predicate)
  }
}


Create a SwiftUI project, add these 3 files, and build the project. If you get this error, it means you're on the right track (in receiving the error):

Cannot convert value of type 'PredicateExpressions.Conjunction<PredicateExpressions.
Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<CompanyAndLocation>, Company>, 
PredicateExpressions.KeyPath<PredicateExpressions.Value<DataView>, Company>>, 
PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<CompanyAndLocation>, 
Location>, PredicateExpressions.KeyPath<PredicateExpressions.Value<DataView>, 
Location>>>' to closure result type 'any StandardPredicateExpression<Bool>'


This error means you're trying to compare complex objects directly, when #Predicate only accepts comparisons of scalar types (Int, String, UUID, etc.). It also means the Predicate closure is returning an internal DSL type that isn't accepted as any StandardPredicateExpression, usually due to incorrect predicate construction.


The problem is in this part of the code

let predicate = #Predicate<CompanyAndLocation> 
{
  $0.company == self.company && $0.location == self.location
}


You could solve it like this:

let companyName = company.name
let locationName = location.location
let predicate = #Predicate<CompanyAndLocation> 
{
  $0.company.name == companyName && $0.location.location == locationName
}


If you don't need to compare the whole object. But what if you do? For two properties, it's simple, but what if this object had more properties? And what if it gets updated with more properties over time? You'd have to revisit this code to update it and ensure a complete comparison.


You could try:

let predicate = #Predicate<CompanyAndLocation> 
{
  $0.company.id == company.id && $0.location.id == location.id
}


Here you're trying to access company.id and location.id directly. The macro can't track these dynamic properties of external objects. It expects simple, scalar values that can be captured as constants. In other words, it doesn't work.


The solution: Compare identifiers, not objects

This is a common issue with Swift's #Predicate macro. The problem is in how the macro captures variables. #Predicate works through type-safe query generation, meaning it needs to translate your Swift expression into a query that can be executed in the database (CoreData or SwiftData).


So, solve it like this:

let companyId = company.id
let locationId = location.id
let predicate = #Predicate<CompanyAndLocation> 
{
  $0.company.id == companyId && $0.location.id == locationId
}


The variables companyId and locationId are captured by the closure as simple (Equatable) values. The macro can identify them as external constants and substitute them correctly in the query.

Conclusion

#Predicate can only translate to database query expressions that compare scalar values (numbers, strings, UUIDs). Comparisons between complex objects cannot be translated to SQL/data queries.


So, inside a #Predicate, compare identifiers, not objects. Always extract the values you want to compare into simple local variables before using them in the closure. This allows the macro to understand exactly which constants should be compiled into the query.


Happy Coding!


[story continues]


Written by
@herlandro
Senior iOS Engineer | Top Rated ADPList Mentor | GitHub & StackOverFlow Contributor | Hackernoon, Medium & Dev.to Writer |

Topics and
tags
swift|swift-predicate|swiftui|type-predicate-generator|swift-data|swift-tutorial|swiftdata-filtering|swift-database-queries
This story on HackerNoon has a decentralized backup on Sia.
Transaction ID: xv_GdiB-EJ-EPCCEepfLE9zc8xGSLJcEHg4C_HeWdac