Goodbye NSPredicate, hello Realm Swift Query API
Tags: ios, swift, database, mobile
Published: 2021-12-17 19:26:02

## Introduction

I'm not a fan of writing code using pseudo-English text strings. It's a major context switch when you've been writing "native" code. Compilers don't detect errors in the strings, whether syntax errors or mismatched types, leaving you to learn of your mistakes when your app crashes.

I spent more than seven years working at MySQL and Oracle, and still wasn't comfortable writing anything but the simplest of SQL queries. I left to join MongoDB because I knew that the object/document model was the way that developers should work with their data. I also knew that idiomatic queries for each programming language were the way to go.

That's why I was really excited when MongoDB acquired Realm—a leading mobile **object** database. You work with Realm objects in your native language (in this case, Swift) to manipulate your data.

However, there was one area that felt odd in Realm's Swift SDK. You had to use `NSPredicate` when searching for Realm objects that match your criteria. `NSPredicate`s are strings with variable substitution. 🤦‍♂️

`NSPredicate`s are used when searching for data in Apple's Core Data database, and so it was a reasonable design decision. It meant that iOS developers could reuse the skills they'd developed while working with Core Data.

But, I hate writing code as strings.

The good news is that the Realm SDK for Swift has added the option to use type-safe queries through the Realm Swift Query API. 🥳.

You now have the option whether to filter using `NSPredicate`s:

```swift
let predicate = NSPredicate(format: "isSoft == %@", NSNumber(value: wantSoft)
let decisions = unfilteredDecisions.filter(predicate)
```

or with the new Realm Swift Query API:

```swift
let decisions = unfilteredDecisions.where { $0.isSoft == wantSoft }
```

In this article, I'm going to show you some examples of how to use the Realm Swift Query API. I'll also show you an example where wrangling with `NSPredicate` strings has frustrated me.

## Prerequisites

- [Realm-Cocoa 10.19.0](https://github.com/realm/realm-cocoa/releases/tag/v10.19.0)+

## Using The Realm Swift Query API

I have a number of existing Realm iOS apps using `NSPredicate`s. When I learnt of the new query API, the first thing I wanted to do was try to replace some of "legacy" queries. I'll start by describing that experience, and then show what other type-safe queries are possible.

### Replacing an NSPredicate

I'll start with the example I gave in the introduction (and how the `NSPredicate` version had previously frustrated me).

I have an [app to train you on what decisions to make in Black Jack](https://github.com/mongodb-developer/BlackJackTrainer) (based on the cards you've been dealt and the card that the dealer is showing). There are three different decision matrices based on the cards you've been dealt:

- Whether you have the option to split your hand (you've been dealt two cards with the same value)
- Your hand is "soft" (you've been dealt an ace, which can take the value of either one or eleven)
- Any other hand

All of the decision-data for the app is held in [`Decisions`](https://github.com/mongodb-developer/BlackJackTrainer/blob/main/Black%20Jack%20Trainer/Black%20Jack%20Trainer/Model/Decisions.swift) objects:

```swift
class Decisions: Object, ObjectKeyIdentifiable {
   @Persisted var decisions = List<DecisionList>()
   @Persisted var isSoft = false
   @Persisted var isSplit = false
   ...
}
```

[`SoftDecisionView`](https://github.com/mongodb-developer/BlackJackTrainer/blob/main/Black%20Jack%20Trainer/Black%20Jack%20Trainer/Views/Decision%20Matrix/SoftDecisionView.swift) needs to find the `Decisions` object where `isSoft` is set to `true`. That requires a simple `NSPredicate`:

```swift
struct SoftDecisionView: View {
   @ObservedResults(Decisions.self, filter: NSPredicate(format: "isSoft == YES")) var decisions
   ...
}
```

But, what if I'd mistyped the attribute name? There's no Xcode auto-complete to help when writing code within a string, and this code builds with no errors or warnings:

```swift
struct SoftDecisionView: View {
   @ObservedResults(Decisions.self, filter: NSPredicate(format: "issoft == YES")) var decisions
   ...
}
```

When I run the code, it works initially. But, when I'm dealt a soft hand, I get this runtime crash:

```
Terminating app due to uncaught exception 'Invalid property name', reason: 'Property 'issoft' not found in object of type 'Decisions''
```

Rather than having a dedicated view for each of the three types of hand, I want to experiment with having a single view to handle all three.

SwiftUI doesn't allow me to use variables (or even named constants) as part of the filter criteria for `@ObservedResults`. This is because the `struct` hasn't been initialized until after the `@ObservedResults` is defined. To live within SwitfUIs constraints, the filtering is moved into the view's body:

```swift
struct SoftDecisionView: View {
   @ObservedResults(Decisions.self) var unfilteredDecisions
   let isSoft = true

   var body: some View {
       let predicate = NSPredicate(format: "isSoft == %@", isSoft)
       let decisions = unfilteredDecisions.filter(predicate)
   ...
}
```

Again, this builds, but the app crashes as soon as I'm dealt a soft hand. This time, the error is much more cryptic:

```
Thread 1: EXC_BAD_ACCESS (code=1, address=0x1)
```

It turns out that, you need to convert the boolean value to an `NSNumber` before substituting it into the `NSPredicate` string:

```swift
struct SoftDecisionView: View {
   @ObservedResults(Decisions.self) var unfilteredDecisions


   let isSoft = true


   var body: some View {
       let predicate = NSPredicate(format: "isSoft == %@", NSNumber(value: isSoft))
       let decisions = unfilteredDecisions.filter(predicate)
   ...
}
```

Who knew? OK, StackOverflow did, but it took me quite a while to find the solution.

Hopefully, this gives you a feeling for why I don't like writing strings in place of code.

This is the same code using the new (type-safe) Realm Swift Query API:

```swift
struct SoftDecisionView: View {
   @ObservedResults(Decisions.self) var unfilteredDecisions
   let isSoft = true

   var body: some View {
       let decisions = unfilteredDecisions.where { $0.isSoft == isSoft }
   ...
}
```

The code's simpler, and (even better) Xcode won't let me use the wrong field name or type—giving me this error before I even try running the code:

![Xcode showing the error "Binary operator '==' cannot be applied to operands of type 'Query<Boo>' and 'Int'](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/xcode_type_warning_5e1c3b18e6.png "Xcode showing the error Binary operator '==' cannot be applied to operands of type 'Query<Bool>' and 'Int'")

### Experimenting With Other Sample Queries

In my [RCurrency app](https://github.com/realm/RCurrency), I was able to replace this `NSPredicate`-based code:

```swift
struct CurrencyRowContainerView: View {
   @ObservedResults(Rate.self) var rates
   let baseSymbol: String
   let symbol: String

   var rate: Rate? {
       NSPredicate(format: "query.from = %@ AND query.to = %@", baseSymbol, symbol)).first
   }
   ...
}
```

With this:

```swift
struct CurrencyRowContainerView: View {
   @ObservedResults(Rate.self) var rates
   let baseSymbol: String
   let symbol: String

   var rate: Rate? {
       rates.where { $0.query.from == baseSymbol && $0.query.to == symbol }.first
   }
   ...
}
```

Again, I find this more Swift-like, and bugs will get caught as I type/build rather than when the app crashes.

I'll use this simple `Task` `Object` to show a few more example queries:

```swift
class Task: Object, ObjectKeyIdentifiable {
   @Persisted var name = ""
   @Persisted var isComplete = false
   @Persisted var assignee: String?
   @Persisted var priority = 0
   @Persisted var progressMinutes = 0
}
```

All in-progress tasks assigned to name:

```swift
let myStartedTasks = realm.objects(Task.self).where {
   ($0.progressMinutes > 0) && ($0.assignee == name)
}
```

All tasks where the `priority` is higher than `minPriority`:

```swift
let highPriorityTasks = realm.objects(Task.self).where {
   $0.priority >= minPriority
}
```

All tasks that have a `priority` that's an integer between `-1` and `minPriority`:

```swift
let lowPriorityTasks = realm.objects(Task.self).where {
   $0.priority.contains(-1...minPriority)
}
```

All tasks where the `assignee` name string includes `namePart`:

```swift
let tasksForName = realm.objects(Task.self).where {
   $0.assignee.contains(namePart)
}
```

### Filtering on Sub-Objects

You may need to filter your Realm objects on values within their sub-objects. Those sub-object may be `EmbeddedObject`s or part of a `List`.

I'll use the `Project` class to illustrate filtering on the attributes of sub-documents:

```swift
class Project: Object, ObjectKeyIdentifiable {
   @Persisted var name = ""
   @Persisted var tasks: List<Task>
}
```

All projects that include a task that's in-progress, and is assigned to a given user:

```swift
let myActiveProjects = realm.objects(Project.self).where {
   ($0.tasks.progressMinutes >= 1) && ($0.tasks.assignee == name)
}
```

### Including the Query When Creating the Original Results (SwiftUI)

At the time of writing, this feature wasn't released, but it can be tested using [this PR](https://github.com/realm/realm-cocoa/pull/7518).

You can include the where modifier directly in your `@ObservedResults` call. That avoids the need to refine your results inside your view's body:

```swift
@ObservedResults(Decisions.self, where: { $0.isSoft == true }) var decisions
```

Unfortunately, SwiftUI rules still mean that you can't use variables or named constants in your `where` block for `@ObservedResults`.

## Conclusion

Realm type-safe queries provide a simple, idiomatic way to filter results in Swift. If you have a bug in your query, it should be caught by Xcode rather than at run-time.

You can find more information in the [docs](https://docs.mongodb.com/realm/sdk/ios/examples/filter-data/#std-label-ios-realm-swift-query-api). If you want to see hundreds of examples, and how they map to equivalent `NSPredicate` queries, then take a look at the [test cases](https://github.com/realm/realm-cocoa/blob/v10.19.0/RealmSwift/Tests/QueryTests.swift).

For those that prefer working with `NSPredicate`s, you can continue to do so. In fact the Realm Swift Query API runs on top of the `NSPredicate` functionality, so they're not going anywhere soon.

Please provide feedback and ask any questions in the [Realm Community Forum](https://www.mongodb.com/community/forums/c/realm-sdks/58).