How to handle optional values ​​in SwiftData predicates

SwiftData has reworked the engine for creating data models, including a safe way to create predicates based on model code. As a result, developers encounter many operations involving optional values ​​when constructing predicates for SwiftData. This article will explore some techniques and considerations for handling optional values ​​during predicate construction.

From “inside out” to “outside in” transformation.

Among the many innovations in SwiftData, the most striking is allowing developers to declare data models directly through code. In Core Data, developers must first create a data model in Xcode’s model editor (corresponding to NSManagedObjectModel) before writing or automatically generating NSManagedObject subclass code.

This process essentially transforms from a model (“inside”) to type code (“outside”). Developers could adapt the type code to some extent, such as change Optional to Non-Optional or NSSet to Setto optimize the development experience, provided these changes do not affect the mapping between the code and the Core Data model.

SwiftData’s clean code declaration method completely changes this process. In SwiftData, the declaration of type code and data model is done simultaneously, or more precisely, SwiftData automatically generates the corresponding data models based on the type code declared by the developers. The declaration method has shifted from the traditional “inside out” to “outside in”.

Optional values ​​and predicates

In the process of creating predicates for the underlying data, the predicate expressions have no direct connection to the type code. The properties used in these expressions correspond to those defined within the model editor (data model), and their “optional” characteristic does not conform to the concept of optional types in Swift, but indicates whether an SQLite field can NULL. This means that when a predicate expression includes a NULLable property and a non-NULL value, its optionality should not normally be considered.

public class Note: NSManagedObject 
    @NSManaged public var name: String?


let predicate = NSPredicate(format: "name BEGINSWITH %@", "fat")

However, the advent of SwiftData is changing this scenario. Because the SwiftData predicate construct is based on model code, the optional types in it truly embody the concept of optionality in Swift. This requires special attention to the handling of optional values ​​when constructing the predicate.

Consider the following SwiftData code example, where improper handling of optional values ​​will lead to compilation errors:

@Model
final class Note 
  var name: String?
  init(name: String?) 
    self.name = name
  


let predicate1 = #Predicate<Note>  note in
  note.name.starts(with: "fat")  // error 

// Value of optional type 'String?' must be unwrapped to refer to member 'starts' of wrapped base type 'String'

let predicate2 = #Predicate<Note>  note in
  note.name?.starts(with: "fat")  // error 

// Cannot convert value of type 'Bool?' to closure result type 'Bool'

Therefore, the correct handling of optional values ​​becomes a critical consideration when creating predicates for SwiftData.

Correct handling of optional values ​​in SwiftData

Although the construction of predicates in SwiftData is similar to writing a closure that returns a boolean value, developers can only use the operators and methods specified in the official documentation, which are converted to the appropriate PredicateExpressions through macros. For optional type name above property, developers can process it using the following methods:

Method 1: Using optional chaining and the zero coalescence operator

By combining optional chaining (?.) with the null join operator (??), you can provide a default Boolean value when the property nil.

let predicate1 = #Predicate<Note>  
  $0.name?.starts(with: "fat") ?? false

Method 2: Using optional binding

With optional binding (if let), you can execute certain logic when the property is not null or return false otherwise.

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

Note that the predicate body can contain only one expression. Therefore, attempt to return another value outside if will not construct a valid predicate:

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

The restriction here means that if else and if each structure is considered a single expression, and each has a direct correspondence with PredicateExpressions. In contrast, the additional return outside the an if structure corresponds to two different expressions.

Although only one expression can be included in the closure predicate, complex query logic can still be constructed by nesting.

Method 3: Use flatMap method

The flatMap method can handle optional values, applying the given closure when not nilprovided that the result can still yield the default value using the nil-coalescing operator.

let predicate3 = #Predicate<Note> 
  $0.name.flatMap  $0.starts(with: "fat")  ?? false

The above strategies provide safe and efficient ways to correctly handle optional values ​​in the SwiftData predicate construct, thus avoiding compilation or runtime errors and ensuring the accuracy and stability of data queries.

Incorrect approach: using forced unwinding

Even if the developer is sure that the property is not null, using ! forcing unwrapping into SwiftData predicates can still lead to runtime errors.

let predicate = #Predicate<Note> 
  $0.name!.starts(with: "fat") // error


// Runtime Error: SwiftData.SwiftDataError._Error.unsupportedPredicate

Handling optional values ​​in special cases

When constructing predicates in SwiftData, although specific methods for handling optional values ​​are generally required, there are some special cases where the rules are slightly different.

Direct comparison of equality

SwiftData allows direct comparison of equality (==) operations that include optional values ​​without requiring additional optionality handling. This means that even if the property is of an optional type, it can be compared directly, as shown below:

let predicate = #Predicate<Note> 
  $0.name == "root"

This rule also applies to comparisons of optional relationship properties between objects. For example, in a one-to-one electoral relationship between Item and Notea direct comparison can be made (even if name is also an optional type):

let predicate = #Predicate<Item> 
  $0.note?.name == "root"

Special cases with optional chaining

Although there is no need for special handling in equality comparisons when the option chain contains only one ?situations involving multiple ?in the chain, even though the code compiles and runs without errors, SwiftData cannot retrieve the correct results from the database via such a predicate.

Consider the following scenario, where there is a one-to-one electoral relationship between Item and Noteand also in between Note and Parent:

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

To solve this problem, it is necessary to ensure that the option chain contains only one ?. This can be achieved by partially unwinding the selection chain, for example:

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

Or:

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

Conclusion

In this article, we explored how to properly handle optional values ​​in the predicate construction process in SwiftData. By introducing various methods, including the use of optional chaining and the nil-coalescing operator, optional linking and flatMap method, we have provided strategies for effective option handling. Moreover, we have highlighted the special cases of directly comparing the equality of optional values ​​and the special handling required when the optional chain contains more ?s. These tips and considerations are intended to help developers avoid common pitfalls, ensuring the construction of accurate and efficient data query predicates, thereby taking full advantage of SwiftData’s powerful features.

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *