A Swift macro taking away the pain in adding ordered relationships to SwiftData models.
Many SwiftData projects require explicitly ordered relationships. SwiftData-OrderedRelationship
is a Swift macro that takes away the pain in implementing this and has implicit CloudKit Sync Conflict Resolution. No special handling on your part is required, when an array with a new order is supplied, OrderedRelationship
does all the work for you.
OrderedRelationship is designed to support CloudKit synchronization, providing fast syncing and implicit conflict resolution.
OrderedRelationship doesn't store the order of its elements as the index of each element. It rather stores each elements position as a random number between Int.min
and Int.max
. When B
gets inserted between A
and C
with the positions 100 and 1041, the position of B
will be randomly chosen between 101, 102 and 103. This means only one object needs to be synced per change.
As detailed in fast syncing, elements do not have consecutive position numbers, but are distributed randomly between Int.min
and Int.max
. By randomly choosing a new number between the numbers inbetween which a new element is inserted or an existing one is moved, having the same operation take place on two different devices with detached state does not require any explicit conflict resolution or re-assigning of indices after both changes have synced to either device.
Say you have a SubItem
model:
@Model
class SubItem {
init() {}
}
Then you can add an ordered relationship to its container Item
:
@Model
final class Item {
@OrderedRelationship
var orderedSubItems: [OrderedSubItem]? = []
init() {}
}
The type of the variable is an optional array of an undefined type, preferably the type you want to be ordered with a single word prefix. This type (OrderedSubItem
in this example) will be defined by the @OrderedRelationship
macro.
Now you add the inverse relationship to the SubItem
model:
var superitem: Item.OrderedSubItem? = nil
That's it. There is nothing more you need to do.
The resulting code will contain not only the OrderedSubItem
model, but also a new variable:
var subItems: [SubItem]
The variable name is inferred from your custom variable name, removing the first word. You can also specify it explicitly using the arrayVariableName
argument. You can both get and set this array. All the work of storing the new order will be performed for you.
The @OrderedRelationship
macro supports arguments to customize its behavior. All of them are optional, as seen in the example above.
The name of the class containing the declaration (Item
in the example above). If nil
, this will be inferred by the name of the file in which the macro resides.
The name of the class that the items are. If nil
, will be inferred by name of the declared array contents without the prefix. In the example above with the array contents being of type OrderedSubItem
, the type should be SubItem
.
The name of the property in itemClassName
to use for the inverse relationship. Defaults to superitem
, and in the example above would be SubItem.superitem
. You are always required to create a property for the inverse relationship, and this argument allows you to tell the macro what it is.
The variable name of the resulting array. If nil
, will be inferred by the name of the declared variable without the prefix. In the example above with the variable name being called orderedSubItems
, the resulting array will be called subItems
.
The delete rule to apply to the items. The default value is .cascade
.
You might wonder why the @OrderedRelationship
macro is applied to the stored array and not the resulting array, like this:
@Model
final class Item {
@OrderedRelationship
var subItems: [SubItem]
init() {}
}
While this code is easier to understand, it is simply an impossibility to create such a macro. Macros are expanded alongside each other on a code level basis. This means that the first macro to be expanded in the above code would be the @Model
macro. The @Model
macro cannot see the results of the @OrderedRelationship
macro, since it is expanded after the @Model
macro. That is why the stored array has to be the one that is declared by the macro user.
Footnotes
-
The positions being this close to each other is a statistically irrelevant event, this is just an example to showcase the method. ↩