Some Quirks of SwiftData with CloudKit

We recently used SwiftData when building our new application Ideate. We use it to persist user data and also to back that data up to iCloud and sync it between devices. All in all, SwiftData is very easy to use. There were a few quirks we discovered with it that we wanted to point out here. The following quirks are only in place when using SwiftData with CloudKit. If you are only using it for local persistence, you don't need to worry about these things.

1 - Optional or Default on all Fields

When you have a class that is marked with @Model, SwiftData knows to treat this class as a part of your data model. Something to be aware of is that in these classes, every member must be either optional or given a default value. This goes back to a CloudKit requirement in which all fields must be optional. The following class is an example of a model that is NOT valid when using CloudKit.

@Model
final class Person {
  var id: UUID
  var name: String
  var age: Int
}

If we want to make this model valid, we need to give the fields default values or make them optional like so:

@Model
final class Person {
  var id: UUID = UUID() // default value
  var name: String = "" // default value
  var age: Int? // optional
}

There are some cases where you are okay having a field be optional, but others where the field is really required and you'd rather have a default instead. The default will be used, for example, with old records that don't have a new field that has been added in newer versions of the schema.

2 - Relationships must be Optional

Whenever you have one data model that has a relationship with another one, the member that has the relationship must be optional. This again goes back to a requirement imposed by CloudKit. The following is an example of a valid model:

@Model
final class Building {
    @Relationship(deleteRule: .cascade, inverse: \Room.building)
    var rooms: [Room]? // Note this is optional
}

The issue here is that working with an optional array all over your application leads to a lot of unwrapping or other techniques that get repetitive. One way to help with this is adding computed properties to work with the array as though it wasn't optional like so:

@Model
final class Building {
    @Relationship(deleteRule: .cascade, inverse: \Room.building)
    var _rooms: [Room]? // Note this is optional

    var rooms: [Room] {
        return _rooms ?? []
    }

    var sortedRooms: [Room] {
        return rooms.sorted(by: {$0.number < $1.number})
    }

    // etc
}

Now when working with your data you can refer to these computed properties. However, you will still need to refer to the _rooms field when making modifications, this example is really only helpful when reading your data rather than when writing.

3 - No Unique Constraints Allowed

CloudKit does not support unique constraints and as such you can not use them in any of your SwiftData models. This is likely not really an issue for your app practically speaking, but you need to be aware of this and structure your data accordingly.

Conclusion

This is by no means an exhaustive list of all the various things one may run into when using SwiftData with CloudKit, but hopefully having this up here will help someone out by showing them a few things to be aware of when getting started with these technologies.