The Problem:
When using @Model
macro on two classes, an array of one class as a property in the another class, the order of the items in the array becomes random. It is not clear if this behavior is intended or a bug.
The Solutions:
Solution 1: Sorting by Timestamp
SwiftUI seems to have an issue with the order of elements in an array when using the @Model
macro. This can be observed when adding items to an array in a @Model
class, as the elements appear in a random order. To resolve this issue, you should add a timestamp to your SwiftData
model and sort the elements by that timestamp in your view.
Here’s how to implement this solution:
-
Add a timestamp property to your
@Model
class:@Model class YourObject { let timestamp: Date // Other variables }
-
In your view, fetch the objects using a query and sort them by the timestamp:
@Query private var yourObjects: [YourObject] List { ForEach(yourObjects.sorted(by: {$0.timestamp < $1.timestamp})) { object in // Other code here } }
By sorting the elements by timestamp, you can ensure that they appear in the order they were added, regardless of the @Model
macro’s behavior.
Solution 2: Implementing Timestamp Sorting with Computed Property
To address the issue of random ordering in arrays when using the `@Model` macro in SwiftData for iOS 17, we can employ a timestamp sorting approach coupled with a computed property in the affected model class (`TestModel`). This solution involves adding both a timestamp/order variable to the nested model class (`TestModel2`) and a computed property to the parent model class (`TestModel`) to return a sorted array.
Here’s how it works:
@Model
class TestModel {
var name: String?
var unsortedArray: [TestModel2]
var sortedArray: [TestModel2] {
return unsortedArray.sorted(by: {$0.order < $1.order})
}
init(name: String = "") {
self.name = name
self.unsortedArray = []
}
}
@Model
class TestModel2 {
var name: String?
var order: Int
init(name: String = "", order: Int = 0) {
self.name = name
self.order = order
}
}
-
In the
TestModel2
class, we introduce a new variable calledorder
that serves as a timestamp or order variable. This variable will be used for sorting the array items. -
In the
TestModel
class, we add a computed property calledsortedArray
. This property utilizes the Swiftsorted(by:)
method to sort theunsortedArray
based on theorder
property ofTestModel2
objects. The sorting is done in ascending order, ensuring that items with lowerorder
values appear first in the sorted array. -
The
unsortedArray
property continues to hold the items in the order they were added, while thesortedArray
property provides a sorted representation of the array when needed. This allows us to maintain the original order of items while also being able to display a sorted version when desired.
Using this approach, you can manually assign order
values to TestModel2
objects to control their position within the sorted array. This gives you more flexibility in organizing and displaying the items in your array.
Solution 3: Using a separate array to store the order
Another approach to maintaining the order of elements in an array is to use a separate array to store the order information. This can be useful when you have complex sorting requirements or when you want to be able to reorder the elements without having to update the actual array.
The Model
@Model
class Model1 {
private var model2sUnsorted: [Model2] // actual model
private var model2Orders: [Model2Order] = []
var model2s: [Model2] {
get {
model2sUnsorted.ordered(by: model2Orders.sorted(by: <).map({ $0.id }))
}
set {
model2sUnsorted = newValue
model2Orders = newValue.map({ Model2Order(id: $0.id) }).ordered()
}
}
init() {
self.model2sUnsorted = []
}
}
@Model
class Model2 {
var name: String
var model1: Model1?
init(name: String = "", model1: Model1? = nil) {
self.name = name
self.model1 = model1
}
}
// MARK: - Reordering with another array: see https://stackoverflow.com/questions/43056807/sorting-a-swift-array-by-ordering-from-another-array
extension Model2: Reorderable {
typealias OrderElement = PersistentIdentifier?
var orderElement: OrderElement { id }
}
protocol Reorderable {
associatedtype OrderElement: Equatable
var orderElement: OrderElement { get }
}
extension Array where Element: Reorderable {
func ordered(by preferredOrder: [Element.OrderElement]) -> [Element] {
sorted {
guard let first = preferredOrder.firstIndex(of: $0.orderElement) else {
return false
}
guard let second = preferredOrder.firstIndex(of: $1.orderElement) else {
return true }
return first < second
}
}
}
// MARK: - Saving the Order
struct Model2Order: Codable, Comparable, Ordered {
var id: PersistentIdentifier
var order: Int?
static func < (lhs: Model2Order, rhs: Model2Order) -> Bool {
guard let first = lhs.order else { return false }
guard let second = rhs.order else { return true }
return first < second
}
}
protocol Ordered {
var order: Int? { get set }
}
extension Array where Element: Ordered {
func ordered() -> [Element] {
var arr = self
for index in arr.indices { arr[index].order = index }
return arr
}
}
The View
Button("Add") {
let newModel2 = Model2()
modelContext.insert(newModel2)
do {
try modelContext.save() // CAVEAT: must save before manipulating the array, because the id seems to be different before and after saving
} catch {
print(error.localizedDescription)
}
if let siblings = model2.model1?.model2s,
let index = siblings.firstIndex(of: model2) {
let nextIndex = siblings.index(after: index)
model2.model1?.model2s.insert(newModel2, at: nextIndex)
do {
try modelContext.save()
} catch {
print(error.localizedDescription)
}
}
}
Solution 4: Workaround for correct ordering
Sorting after the fact with a computed property is fine for read-only display, but it becomes more problematic when the ordering is crucial, and users need a way to reorder the items and save that reordering.
Workaround:
The only workaround found is not ideal and involves making a temp copy of the array and looping through it to manually update the ordering. This approach is inefficient and inelegant for a task that should be simple using SQL’s ORDER BY
clause.
SQL approach (unavailable):
The ideal solution would be to use SQL’s ORDER BY
clause to sort the items. However, this is not possible, as the Query
macro does not allow mixed types for sorting.
Thus, the workaround remains the only viable option until Apple provides a better solution or allows mixed types in the Query
macro.
Q&A
Why does @Model cause the order of SwiftData’s array to be random?
A bug in SwiftData causes the order of arrays in @Model to be randomized.
Is there a workaround for the SwiftData array randomization bug?
Add a timestamp to the model and sort by that instead.
Can you provide a code example for sorting an array by timestamp?
Sure, here’s an example using a timestamp to sort an array:
Video Explanation:
The following video, titled "iOS Swift Tutorial - Working with the Web - How to parse JSON into ...", provides additional insights and in-depth exploration related to the topics discussed in this post.
In this Swift tutorial, you will learn how to work with network services and APIs, and how to parse JSON into model objects that can be used ...
The following video, titled "iOS Swift Tutorial - Working with the Web - How to parse JSON into ...", provides additional insights and in-depth exploration related to the topics discussed in this post.
In this Swift tutorial, you will learn how to work with network services and APIs, and how to parse JSON into model objects that can be used ...