SwiftData IOS 17 Array in random order? – Ios

by
Ali Hasan
axios beta swift-concurrency swift-data

Quick Fix: In SwiftUI’s List view, ensure that your data is sorted by a unique timestamp field to guarantee consistent ordering of elements.

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:

  1. Add a timestamp property to your @Model class:

    @Model
    class YourObject {
        let timestamp: Date
        // Other variables
    }
    
  2. 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 called order 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 called sortedArray. This property utilizes the Swift sorted(by:) method to sort the unsortedArray based on the order property of TestModel2 objects. The sorting is done in ascending order, ensuring that items with lower order values appear first in the sorted array.

  • The unsortedArray property continues to hold the items in the order they were added, while the sortedArray 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.

Play video

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 ...