The Ultimate Guide on How to use Predicate in Swift

Needone App
14 min readFeb 10, 2025

--

NSPredicate has always been a powerful tool provided by Apple, allowing developers to filter and evaluate data collections with complex logical conditions in a natural and efficient way. As Swift continues to mature and evolve, the 2023 Swift community undertook a major update to re-implement the Foundation framework using pure Swift language. This significant update introduced new Predicate functionality coded in Swift, marking a new stage in data processing and evaluation.

What is a predicate

In modern software development, efficient and accurate filtering and evaluation of data are crucial. A predicate (Predicate) is a powerful tool that allows developers to achieve this by defining logical conditions that return Boolean values (true or false). This plays a core role not only in filtering collections or finding specific elements within them but also as the foundation for data processing and business logic implementation.

Although Apple’s NSPredicate provides this capability, it relies on Objective-C syntax, poses risks of runtime errors, and faces platform limitations that restrict its application scope and flexibility.

class MyObject: NSObject {
@objc var name: String
init(name: String) {
self.name = name
}
}
let object = MyObject(name: "fat")

// create NSPredicate
let predicate = NSPredicate(format: "name = %@", "fat")
XCTAssertTrue(predicate.evaluate(with: object)) // true

let objs = [object]
// filter object by predicate
let filteredObjs = (objs as NSArray).filtered(using: predicate) as! [MyObject]
XCTAssertEqual(filteredObjs.count, 1)

Introduction and Improvement of Swift Predicate

To overcome these limitations and expand the application scope of predicates, the Swift community re-implemented the Foundation framework, introducing Predicate functionality coded in Swift. This new feature not only breaks away from Objective-C dependency but also simplifies predicate construction using Swift’s macro capabilities, as shown below:

class MyObject {
var name:String
init(name: String) {
self.name = name
}
}

let object = MyObject(name: "fat")
let predicate = #Predicate<MyObject>{ $0.name == "fat" }
try XCTAssertTrue(predicate.evaluate(object)) // true

let objs = [object]
let filteredObjs = try objs.filter(predicate)
XCTAssertEqual(filteredObjs.count, 1)

In this example, we construct a logical condition using the #Predicate macro. This construction method is similar to writing closure code, allowing developers to build more complex logic in a natural way, such as predicates with multiple conditions:

let predicate = #Predicate<MyObject>{ object in
object.name == "fat" && object.name.count < 3
}
try XCTAssertTrue(predicate.evaluate(object)) // false

Furthermore, the MyObject class no longer needs to inherit from NSObject or use @objc attributes for its properties to support KVC. Swift Predicate is also applicable to types that still inherit from NSObject.

Comparison between NSPredicate and Swift Predicate

Compared to NSPredicate, Swift Predicate offers numerous improvements:

  • Open-source nature and platform compatibility: Supports cross-platform usage, such as on Linux and Windows.

Safety

  • Type Safety: Leveraging Swift’s type checking to reduce runtime errors.
  • Development Efficiency: Benefiting from Xcode support, improving code writing speed and accuracy.
  • Syntax Freedom: Providing greater expressive freedom, not limited by Objective-C syntax rules.
  • Generality: Applicable to all Swift types, no longer limited to classes inheriting from NSObject.
  • Modern Swift Feature Support: Supporting Sendable and Codable features, making it more suitable for current Swift programming paradigms.

Through these improvements, Swift Predicate not only optimizes developers’ workflows but also opens up new paths for the expansion and growth of the Swift ecosystem.

Main Components of Swift Predicate

Before diving into the usage methods and considerations of Swift Predicate, we need to understand its structure. Specifically, we should know what elements make up a predicate and how predicate macros work.

The PredicateExpression Protocol

The PredicateExpression protocol (or a specific type that conforms to it) defines the conditional logic of an expression. For example, it can represent a "less than" condition with specific logical judgments to determine whether an input value is less than a given value. This protocol is the most critical part of building the Swift Predicate architecture. The declaration of the PredicateExpression protocol is as follows:

public protocol PredicateExpression<Output> {
associatedtype Output

func evaluate(_ bindings: PredicateBindings) throws -> Output
}

The Foundation framework provides a series of predefined expression types that conform to the PredicateExpression protocol, allowing developers to directly use types or type methods under PredicateExpressions to construct predicate expressions. This paves the way for building flexible and powerful conditional evaluation logic. For example, if we want to construct an expression representing the number 4, the corresponding code is as follows:

let express = PredicateExpressions.Value(4)

The implementation of PredicateExpressions.Value code is as follows:

extension PredicateExpressions {
public struct Value<Output> : PredicateExpression {
public let value: Output

public init(_ value: Output) {
self.value = value
}

public func evaluate(_ bindings: PredicateBindings) -> Output {
return self.value
}
}
}

The Value struct directly encapsulates a value and, when its evaluate method is called, simply returns the encapsulated value. This makes Value an effective way to represent constant values in predicate expressions.

Note that the PredicateExpression protocol's evaluate method can return any type of value, not limited to Boolean types.

Furthermore, if we need to define a table expression for the condition “3 < 4”, the corresponding code example is as follows:

let express = PredicateExpressions.LessThan(3, 4)

… (rest of the text remains the same)

let express = PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.Value(3),
rhs: PredicateExpressions.Value(4),
op: .lessThan
)

This code snippet generates a type instance that conforms to the PredicateExpression protocol:

PredicateExpressions.Comparison<PredicateExpressions.Value<Int>, PredicateExpressions.Value<Int>>

When calling the evaluate method on this instance, it returns a Boolean value representing the result of the comparison.

Developers can build complex logical judgments by nesting expressions. The resulting type expressions also become correspondingly complex.

June 2024 update

At WWDC 2024, the Foundation’s predicate system introduced several new features, including new expression methods and the most significant improvement: the addition of the #Expression macro. This greatly simplifies the process of building predicate expressions, even when building standalone expressions. In previous versions, developers could only experience this convenience when using the #Predicate macro.

The #Expression macro allows developers to define predicates using multiple independent expressions, making it easier to build complex predicates and increasing the reusability of expressions.

Unlike predicates, which can only return a Boolean value, expressions can return any type. Therefore, when declaring an expression, developers must explicitly specify the input and output types.

let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
items.filter {
!$0.isInPlan
}.count
}

let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip
// The current date falls within the trip
(trip.startDate ..< trip.endDate).contains(today) &&

// The trip has at least one BucketListItem
// where 'isInPlan' is false
unplannedItemsExpression.evaluate(trip.bucketList) > 0
}

Predicate Structure

The Predicate struct, even when defined using a macro, remains the core of predicate logic. This struct is responsible for binding logical conditions (implemented by PredicateExpression) to specific values. This mechanism enables Predicate to instantiate specific condition logic and accept input values for evaluation.

Its definition is as follows:

public struct Predicate<each Input> : Sendable {
public let expression : any StandardPredicateExpression<Bool>
public let variable: (repeat PredicateExpressions.Variable<each Input>)

public init(_ builder: (repeat PredicateExpressions.Variable<each Input>) -> any StandardPredicateExpression<Bool>) {
self.variable = (repeat PredicateExpressions.Variable<each Input>())
}

Main Features

  • Boolean Return Value Limitation: The Predicate class is designed to handle expressions that return boolean values. This means that the final result of a complex expression tree must be a boolean value in order to facilitate logical judgments.
  • Construction Process: When creating a Predicate, you must provide a closure that takes parameters of type PredicateExpressions.Variable and returns an expression that conforms to the StandardPredicateExpression<Bool> protocol.
  • The StandardPredicateExpression Protocol: This is an extension of the PredicateExpression protocol, requiring expressions to also conform to Codable and Sendable. Currently, only Foundation-provided expressions are allowed to comply with this protocol.
public protocol StandardPredicateExpression<Output> : PredicateExpression, Codable, Sendable {}
  • Advanced Features of Closure and Variable Property: Leveraging Swift’s Parameter Packs feature, Predicate supports creating predicates that can handle multiple generic parameters simultaneously, a functionality not available in NSPredicate.

For example, using the Predicate struct and PredicateExpression protocol, you can create a predicate to compare two integers n and m (n < m) as follows:

// Define a closure: compares two integer values whether they satisfy "less than" relationship.
// This closure takes two PredicateExpressions.Variable<Int> type parameters,
// and constructs a PredicateExpression representing the "less than" comparison logic.
let express = { (value1: PredicateExpressions.Variable<Int>, value2: PredicateExpressions.Variable<Int>) in
PredicateExpressions.build_Comparison(
lhs: value1,
rhs: value2,
op: .lessThan
)
}

// Use the express closure to construct a Predicate instance,
// where express defines the evaluation logic, i.e., whether the first parameter is less than the second.
let predicate = Predicate {
express($0, $1)
}

let n = 3
let m = 4

// Evaluate the predicate: check if n is less than m, expecting a return of true.
try XCTAssertTrue(predicate.evaluate(n, m))

The Predicate Macro

Compared to building predicates using strings like with NSPredicate, although directly using PredicateExpression and Predicate structs can provide type safety checks and auto-completion advantages, this approach is less efficient and more difficult to write and read, undoubtedly increasing the cognitive burden on developers when creating predicates.

To alleviate these complexities, Foundation introduces the Predicate macro (#Predicate), aiming to help developers construct Swift Predicates in a more concise and efficient manner.

Using the same example of building a predicate to judge whether n < m, the macro significantly simplifies the construction process:

let predicate = #Predicate<Int,Int>{ $0 < $1}

In Xcode, by viewing the expanded macro code, we can clearly see how the macro simplifies the previously complex logic that required a large amount of code.

image-20240225182917655

The Predicate macro’s implementation code is approximately 1200 lines long and only supports pre-built predicate expressions in Foundation, as well as specific methods that can be used within predicates. When converting, errors will occur if an unsupported expression type or method is encountered, or if no corresponding expression exists.

By introducing the Predicate macro, Swift provides a concise yet powerful way to build complex predicate logic, allowing developers to directly construct complex logical judgments in almost native Swift code form, significantly improving code readability and maintainability. More importantly, using the Predicate macro greatly reduces the cognitive burden on developers when building complex queries, making the development workflow smoother and more efficient.

Techniques and Considerations for Building Swift Predicates

After understanding the composition of Swift Predicates, we can better grasp the restrictions and techniques involved in building predicates.

Restrictions on Global Functions

When using the Predicate macro to build predicates, note that the conversion logic converts closure code into Foundation’s pre-built PredicateExpress expressions. The current implementation of PredicateExpress does not support direct access to global functions or type methods or properties returning data. Therefore, when using such data to construct predicates, it is necessary to use the let keyword to first obtain the desired data. For example:

func now() -> Date {
.now
}
let predicate = #Predicate<Date>{ $0 < now() } // Global functions are not supported in this predicate

The correct way is to first get the function or property value, then construct the predicate:

let now = now()
let predicate = #Predicate<Date>{ $0 < now }

Similarly, direct access to type properties is also restricted:

let predicate = #Predicate<Date>{ $0 < Date.now  }
// Key path cannot refer to static member 'now'

let now = Date.now
let predicate = #Predicate<Date>{ $0 < now }

This is because the current predicate expression only supports instance property KeyPaths and does not support type properties.

Restrictions on Instance Methods

Similarly, direct calls to instance methods (such as .lowercased()) are also unsupported in predicates.

struct A {
var name:String
}

let predicate = #Predicate<A>{ $0.name.lowercased() == "fat" } // The lowercased() function is not supported in this predicate

In such cases, it is necessary to use the built-in methods supported by Swift Predicate, for example:

let predicate = #Predicate<A>{ $0.name.localizedLowercase == "fat" }

Currently, the available set of built-in methods is relatively limited and includes but is not limited to: contains, allSatisfy, flatMap, filter, subscript, starts, min, max, localizedStandardContains, localizedCompare, and caseInsensitiveCompare. Developers should regularly review Apple's official documentation or directly refer to the source code of the Predicate macro for a comprehensive understanding of the latest supported methods.

Since the built-in methods are not exhaustive, some common predicate construction patterns in NSPredicate may not be supported in Swift Predicate. This means that although Swift Predicate provides powerful tools for building type-safe and expressive predicates, developers may still need to find alternative solutions or wait for future extensions to cover a wider range of use cases.

Supporting the creation of predicates with multiple generic parameters

Thanks to the Parameter Packs feature, Swift Predicate offers more flexibility by allowing developers to define predicates that can accept multiple generic parameters. This capability greatly extends the applicability of predicates and enables developers to easily handle various complex conditional judgment requirements.

As shown in the n < m example constructed earlier, this method not only applies to single-type parameter comparisons but can also be extended to multiple different types of parameters, further enhancing Swift Predicate's expressiveness and flexibility compared to traditional Swift higher-order functions. This feature makes Swift Predicate a powerful tool for building complex logic judgments while maintaining code clarity and type safety.

struct A {
var name: String
}

struct B {
var age: Int
}

let predicate = #Predicate<A, B> { a, b in
!a.name.isEmpty && b.age > 10
}

Creating complex judgment logic through nesting mechanisms

The design of Swift Predicate allows developers to build structured predicates by nesting predicate expressions. This capability makes it easier and more intuitive to implement conditional judgments that would typically rely on subqueries in NSPredicate. Today, these complex logic expressions can be written in a way that is more consistent with the programming habits of the Swift language, improving code readability and maintainability.

struct Address {
var city: String
}

struct People {
var address: [Address]
}

let predicate = #Predicate<People> { people in
people.address.contains { address in
address.city == "Dalian"
}
}

Note that the above methods do not work when the data model contains multiple relationships and is optional.

Supporting the construction of predicates with optional values

Swift Predicate supports the use of optional value types, which is a significant advantage when handling optional properties in data models. This support allows developers to directly handle optional values within predicate logic, making it easier to write predicate expressions that are more direct and clear.

For example, the following example demonstrates how to process an optional string property in Swift Predicate based on whether it starts with a specific prefix:

let predicate = #Predicate<Note> {
if let name = $0.name {
return name.starts(with: "fat")
} else {
return false
}
}

For developers who want to delve deeper into how to efficiently handle optional values in Swift Predicate, we recommend reading How to Handle Optional Values in SwiftData Predicates.

Swift Predicate is thread-safe

Swift Predicate’s design takes into account the needs of concurrent programming, ensuring its thread safety. By conforming to the Sendable protocol, Swift Predicate supports safe transfer between different execution contexts. This feature significantly enhances the utility of Swift Predicate, making it adaptable to the widespread demands for concurrency and asynchronous programming in modern Swift applications.

Support for Serialization and Deserialization

By implementing the Codable protocol, Swift Predicate can be converted into JSON or other formats, enabling data serialization and deserialization. This feature is particularly important for scenarios where predicate conditions need to be saved to a database or configuration file, or shared between clients and servers.

The following example demonstrates how to serialize a Predicate instance as JSON data, which can then be stored or transmitted:

struct A {
var name:String
}

let predicate = #Predicate<A>{ $0.name == "fatbobman" }
var configuration = Predicate<A>.EncodingConfiguration.standardConfiguration
configuration.allowKeyPath(\A.name, identifier: "name")
let data = try JSONEncoder().encode(predicate, configuration: configuration)

Considerations for Complex Predicates

When building complex predicates, be aware of their impact on compilation time. Similar to the situation when building interfaces in SwiftUI, constructing complex Swift Predicate expressions requires the Swift compiler to process and transform them into a large and complex type. If the complexity of the expression exceeds a certain threshold, the compiler’s type inference will significantly increase.

When encountering compilation-time issues, developers can consider placing complex predicate declarations in separate Swift files. This not only helps organize and manage code but also reduces the likelihood of triggering recompilation due to frequent modifications elsewhere.

No Support for Custom Predicate Expressions

Currently, although developers can create custom expression types conforming to the PredicateExpress protocol, official support is limited to using pre-built StandardPredicateExpression implementations from Foundation. Therefore, while custom expression types can be created, they cannot be directly used in building predicates with the Predicate macro.

Even if developers annotate their custom expressions as conforming to the StandardPredicateExpression protocol, the Predicate macro only supports using built-in StandardPredicateExpression instances. This limitation prevents developers from utilizing custom expressions in Predicate macros and constructing predicates with them.

No Support for Combining Multiple Predicates

Unlike NSCompoundPredicate, which allows combining multiple simple logical NSPredicates into more complex ones, Swift Predicate currently does not provide a similar capability. This restricts developers’ flexibility when building complex predicates to some extent.

In future articles, I will introduce how to dynamically build complex predicates using the PredicateExpress protocol to meet specific requirements, providing an alternative solution to overcome this limitation.

Applying Swift Predicate in SwiftData

SwiftData and Core Data use Predicates as data retrieval conditions in many development scenarios. Understanding how SwiftData handles Swift Predicate is crucial for maximizing its utility.

Interaction Mechanism between SwiftData and Swift Predicate

When setting a FetchDescriptor with a Predicate in SwiftData, it does not directly adopt the evaluation mechanism of Swift Predicate. Instead, it parses the express property of the predicate to define an expression tree and converts these expressions into SQL statements to retrieve data from the SQLite database. This means that the evaluation operation is actually performed on the database side through SQL instructions.

Restrictions on Predicate Parameters in SwiftData

SwiftData imposes certain restrictions on predicate parameters, which may affect how you use predicates in your application. SwiftData requires each FetchDescriptor to correspond to a specific data entity. Therefore, when building predicates, the corresponding entity type becomes the only parameter of the predicate, which is crucial for effective use of SwiftData in building predicates.

Expression Limitations of SwiftData Predicate Construction

Although Swift Predicate provides a powerful framework for data filtering, its expressiveness in the SwiftData environment has some limitations compared to using NSPredicate with Core Data. When facing specific filtering requirements, developers may need to adopt indirect methods, such as executing multiple filters or adding specific properties to entities beforehand to adapt to the current predicate capabilities. For example, since the built-in starts method is case-sensitive, if you want to implement case-insensitive matching, it is recommended to create a pre-processed version of the filtered property (e.g., by converting all characters to lowercase) to support more flexible data retrieval.

Runtime Errors in Predicates

Even if Swift Predicate does not have compilation errors, using SwiftData for data retrieval may encounter situations where it cannot successfully convert the predicate into an SQL statement, resulting in runtime errors. Consider the following example:

let predicate = #Predicate<Note> { $0.id == noteID }
// Runtime error: Couldn't find \Note.id on Note with fields

Although the Note type conforms to the PersistentModel protocol and its id property is of type PersistentIdentifier, SwiftData cannot recognize the id property when converting the predicate into an SQL instruction. In this case, developers should use the persistentModelID property for comparison (in addition to properties corresponding to underlying data models, persistentModelID is one of the few supported properties):

let predicate = #Predicate<Note> { $0.persistentModelID == noteID }

Additionally, trying to apply built-in methods on PersistentModel attributes may also encounter issues:

let predicate = #Predicate<Note> {
$0.name.localizedLowercase.starts(with: "abc".localizedLowercase)
}
// Runtime error: Couldn't find \Note.name.localizedLowercase on Note with fields

When SwiftData converts these expressions, many built-in methods are not applicable to PersistentModel attributes and are treated as KeyPath by SwiftData. Therefore, developers may need to create additional properties (e.g., the lowercase version of an attribute) to adapt to this scenario.

Failure to Retrieve Expected Results

In some cases, Swift Predicate can compile and run without errors in the SwiftData environment but fail to retrieve the expected results because SwiftData converted incorrect SQL instructions. The following example illustrates this:

let predicate = #Predicate<Item> {
$0.note?.parent?.persistentModelID == rootNoteID
}

This predicate will not report any compilation or runtime errors, but it will ultimately fail to retrieve data correctly. To resolve this issue, we need to build the same logical predicate using a different approach that handles optional values correctly; see How to Handle Optional Values in SwiftData Predicates for details:

let predicate = #Predicate<Item> {
if let note = $0.note {
return note.parent?.persistentModelID == rootNoteID
} else {
return false
}
}

Therefore, conducting thorough and timely unit testing is particularly important when building SwiftData predicates. Through testing, developers can verify whether the predicate’s behavior matches their expectations and ensure the accuracy and stability of data retrieval.

Conclusion

Swift Predicate brings a powerful and flexible tool to Swift developers, making data filtering and logical judgments more intuitive and efficient. Through this article’s exploration, I hope that developers not only master the strong capabilities and usage methods of Swift Predicate but also find creative solutions when facing challenges and limitations.

--

--

No responses yet