DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Commonly Occurring Errors in Microsoft Graph Integrations and How To Troubleshoot Them (Part 4)
  • Unit Testing Large Codebases: Principles, Practices, and C++ Examples
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Thermometer Continuation in Scala

Trending

  • Navigating Change Management: A Guide for Engineers
  • Enhancing Business Decision-Making Through Advanced Data Visualization Techniques
  • Using Python Libraries in Java
  • Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
  1. DZone
  2. Data Engineering
  3. Big Data
  4. How To Handle Optional Values in SwiftData Predicates

How To Handle Optional Values in SwiftData Predicates

This article will explore some techniques and considerations for handling optional values while building SwiftData predicates.

By 
Fatbobman Xu user avatar
Fatbobman Xu
·
Feb. 23, 24 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
4.7K Views

Join the DZone community and get the full member experience.

Join For Free

SwiftData has revamped the mechanism for creating data models, incorporating a type-safe mode for predicate creation based on model code. As a result, developers encounter numerous operations involving optional values when constructing predicates for SwiftData. This article will explore some techniques and considerations for handling optional values while building predicates.

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 auto-generating NSManagedObject subclass code.

This process essentially transforms from the model ("inside") to type code ("outside"). Developers could adjust the type code to some extent, such as changing Optional to Non-Optional or NSSet to Set, to optimize the development experience, provided these modifications do not affect the mapping between Core Data's code and models.

The pure code declaration method of SwiftData completely changes this process. In SwiftData, the declaration of type code and data models is carried out simultaneously, or more accurately, SwiftData automatically generates the corresponding data models based on the type code declared by developers. The method of declaration has shifted from the traditional "inside-out" to "outside-in."

Optional Values and Predicates

In the process of creating predicates for Core Data, the predicate expressions do not have a direct link 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 align with the concept of optional types in Swift but rather indicates whether the SQLite field can be NULL. This means that when a predicate expression involves a property that can be NULL and a non-NULL value, its optionality usually does not need to be considered.

Swift
 
public class Note: NSManagedObject {
    @NSManaged public var name: String?
}

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


However, the advent of SwiftData changes this scenario. Since the construction of SwiftData predicates is based on model code, the optional types therein truly embody the concept of optional in Swift. This necessitates special attention to the handling of optional values when building predicates.

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

Swift
 
@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, correctly handling optional values becomes a critical consideration when constructing predicates for SwiftData.

Correctly Handling Optional Values in SwiftData

Although predicate construction in SwiftData is similar to writing a closure that returns a boolean value, developers can only use the operators and methods listed in the official documentation, which are converted into corresponding PredicateExpressions through macros. For the optional type name property mentioned above, developers can handle it using the following methods:

Method 1: Using Optional Chaining and the Nil-Coalescing Operator

By combining optional chaining (?.) with the nil-coalescing operator (??), you can provide a default boolean value when the property is nil.

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

Method 2: Using Optional Binding

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

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


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

Swift
 
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 structures are each considered a single expression, each having a direct correspondence to PredicateExpressions. In contrast, an additional return outside of an if structure corresponds to two different expressions.

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

Method 3: Using the flatMap Method

The flatMap method can handle optional values, applying a given closure when not nil, with the result still being able to provide a default value using the nil-coalescing operator.

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


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

Incorrect Approach: Using Forced Unwrapping

Even if a developer is certain a property is not nil, using ! to force unwrap in SwiftData predicates can still lead to runtime errors.

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

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

Unprocessable Optional Values

As of now (up to Xcode 15C500b), when the data model includes an optional to-many relationship, the methods mentioned above do not work. For example:

let predicate = #Predicate<Memo>{
      $0.assets?.isEmpty == true
}

// or 

let predicate = #Predicate<Memo>{ $0.assets == nil }

SwiftData encounters a runtime error when converting the predicate into SQL commands:

error: SQLCore dispatchRequest: exception handling request: <NSSQLCountRequestContext: 0x6000038dc620>, to-many key not allowed here with userInfo of (null)

Handling Optional Values in Special Cases

When constructing predicates in SwiftData, while specific methods are generally required to handle optional values, there are some special cases where the rules differ slightly.

Direct Equality Comparison

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

Swift
 
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 optional relationship between Item and Note, a direct comparison can be made (even if name is also an optional type):

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


Special Cases With Optional Chaining

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

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

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


To address this issue, it is necessary to ensure that the optional chain contains only one ?. This can be achieved by partially unwrapping the optional chain, for example:

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


Or:

Swift
 
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 have explored how to handle optional values correctly in the process of constructing predicates in SwiftData. By introducing various methods, including the use of optional chaining and the nil-coalescing operator, optional binding, and the flatMap method, we have provided strategies for effectively handling optionality. Moreover, we highlighted the special cases of direct equality comparison of optional values and the special handling required when an optional chain contains multiple ?s. These tips and considerations are aimed at helping developers avoid common pitfalls, ensuring the construction of accurate and efficient data query predicates, thereby fully leveraging the powerful features of SwiftData.

Core Data Data model (GIS) Data Types

Published at DZone with permission of Fatbobman Xu. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Commonly Occurring Errors in Microsoft Graph Integrations and How To Troubleshoot Them (Part 4)
  • Unit Testing Large Codebases: Principles, Practices, and C++ Examples
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Thermometer Continuation in Scala

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!