SwiftData IOS 17 Array in random order?

1.7k Views Asked by At

Why is the order of my array random when i use the @Model macro.

class TestModel {
var name: String?
var array: \[TestModel2\]

    init(name: String = "") {
        self.name = name
        array = []
    }

}

class TestModel2 {
var name: String?

    init(name: String = "") {
        self.name = name
    }

}

This works fine, and all the items in array are in the order I add them.

But if I declare both of them as @Model, like this:


@Model
class TestModel {
var name: String?
var array: \[TestModel2\]

    init(name: String = "") {
        self.name = name
        array = []
    }

}

@Model
class TestModel2 {
var name: String?

    init(name: String = "") {
        self.name = name
    }

}

The array items are always in a random order. When I reload the view where they are displayed or when I add items to the array the order is randomised.

This behaviour can also be seen in the sample code here. When adding bucket list items to a trip, the items are always displayed in a random order.

Is this a beta bug? Or is this intended?

4

There are 4 best solutions below

0
Joseph Kessler On

I had the same issue. Like the others said in the comments, I had to add a timestamp to my SwiftData model and sort by that. It must be a bug or something.

@Model
final class YourObject {
    let timestamp: Date
    // Other variables
}

And then in your view, you can do something like this:

@Query private var yourObjects: [YourObject]

List {
    ForEach(yourObjects.sorted(by: {$0.timestamp < $1.timestamp})) { object in 
        // Other code here
    }
}
0
jamesyoungSOusername On

To elaborate on the timestamp sorting answer above (Also I think I might have just also replied to you on the Apple Developer Forums), you can add both a timestamp/order variable to your TestModel2 AND a computed property to your TestModel to return your sorted array when you need it instead of needing to manually sort it every time you want to use it in the UI.

@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
    }
}
0
kiu On

New to StackOverflow here and still learning swift. Just thought I'd share the solutions I found (both solutions are working for my use case so far, but do let me know if they don't work for other scenarios).

Solution #1: The model's built-in persistentModelID contains a "url" which appears to be incremented when a model is created and saved (e.g. x-coredata://xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/Model2/p1). Not sure what it's for but the counter stays the same even if another model is deleted.

@Query(sort: \Model2.persistentModelID) var model2s: [Model2]

This is simplest and one can sort at the model level (which is supposed to be the fastest).

Solution #2: Replicating ordered array behaviour by saving another array containing order information in Model1 (Model2 doesn't need to know the order information, which is what my use case requires). Still trying to tidy up the code, but remove/insert/append functions seems to be working so far.

import SwiftData

@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
  }
}

In the view, can do this:

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)
    }
  }
}
0
sym3tri On

Sorting after the fact with a computed property is fine for read-only display, but this is much more of a problem when the ordering is more significant and you need to give users a way to reorder the items and persist that reordering (i.e. .move modifier in a List).

The only workaround I found is quite ugly and requires making temp copy of the array and looping thru it to manually update the ordering.

With regular SQL this would be so easy, just adding another value to the ORDER BY clause :/

And unfortunately this doesn't work b/c the Query macro doesn't allow mixed types:

@Query(sort: [SortDescriptor(\TestModel.timestamp), SortDescriptor(\TestMode2.order)]