20

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?

6
  • Not sure what you have defined in your model, usually one uses @Relationship when two models are connected. Commented Aug 12, 2023 at 16:08
  • 1
    According to the docs @Relationship is only required to specify a custom deletion rule. "When a model contains an attribute whose type is also a model (or a collection of models), SwiftData implicitly manages the relationship between those models for you. By default, the framework sets relationship attributes to nil after you delete a related model instance. To specify a different deletion rule, annotate the property with the Relationship(::originalName:inverse:hashModifier:) macro. " When I add @Relationship`to the model, there is no difference to the outcome. Commented Aug 12, 2023 at 16:46
  • 6
    I must have missed that part, excellent since it will simplify things. SwiftData uses Core Data under the hood and Core Data uses Set for to-many relationships properties so that is most likely why your array property changes order all the time. Commented Aug 12, 2023 at 18:49
  • I also had the same issue so I used a time stamp and sort the data based on them when fetching it. Commented Aug 14, 2023 at 8:59
  • 1
    @user11640506 How do you sort TestModel2 if you have a query on TestModel? Commented Aug 14, 2023 at 9:40

7 Answers 7

3

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
    }
}
Sign up to request clarification or add additional context in comments.

Comments

3

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

Comments

2

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

Comments

0

There are two ways I know of to obtain an ordered collection from a SwiftData multi relationship property:

  1. Create a computed property that returns the value of .sorted(by:) as mentioned in other answers here.
  2. Create a custom FetchDescriptor that returns the values

Comparing the two using the XCTest measure facility, I find that the second is about 3x faster than the first.

My models (abbreviated):

extension SchemaV1 {

  @Model
  public final class SoundFont {
    @Relationship(deleteRule: .cascade, inverse: \Preset.owner) public var presets: [Preset] = []
    public var displayName: String = ""

    public var orderedPresets: [Preset] {
      self.presets.sorted(by: { $0.index < $1.index })
    }
  }

  @Model
  public final class Preset {
    public var owner: SoundFont?
    public var index: Int = -1
    public var name: String = ""

    public init(owner: SoundFont, index: Int, name: String) {
      self.owner = owner
      self.index = index
      self.name = name
    }
  }
}

And the custom query:

public extension ModelContext {

  @MainActor
  func orderedPresets(for soundFont: SoundFont) -> [Preset] {
    let ownerId = soundFont.persistentModelID
    let fetchDescriptor = FetchDescriptor<Preset>(predicate: #Predicate { $0.owner?.persistentModelID == ownerId },
                                                  sortBy: [SortDescriptor(\.index)])
    return (try? fetch(fetchDescriptor)) ?? []
  }
}

I'm actually considering not storing the relationship in SwiftData since I don't see any real benefit right now, especially since cascading deletions often seems to not work as one would expect, at least in my testing.

Comments

0

Use @Transient

    @Model
    class TestModel {
        var name: String?
        @Relationship(deleteRule: .cascade) private var arrayPersistent: [TestModel2]
        @Transient var array: [TestModel2] {
                get {return arrayPersistent.sorted(by: {$0. someIndex! < $1. someIndex!})} 
// Or    
/*              get {return arrayPersistent.sorted(by: {$0. name! < $1. name!})}*/
                set { arrayPersistent = newValue }
            }
      
        
            init(name: String = "") {
                self.name = name
                array = []
            }
        
        }
        
        @Model
        class TestModel2 {
        var name: String?
        var someIndex: Int?
        
            init(name: String = "", someIndex:Int) {
                self.name = name
                self.someIndex = someIndex
            }
        
        }

1 Comment

This solution works but you really don't need the @Transient attribute because SwiftData doesn't store computed properties anyways.
0

Just to add some context: SwiftData uses SQLite under the hood as its persistence engine. SQL queries, by design, return data in a non-deterministic order. It is not a bug. (If you were to type select * from table in a SQL client, the order of the results can be different every time.)

In order to get a deterministic sort order from SwiftData, you must include some sort of sorting parameter(s).

As has been mentioned by other answers, you can use incidental data in your model (like a created timestamp) or you can manually include a "sort" or "position" attribute that you can sort by. A specific sort attribute is by far the most flexible, but also the most work as it must be maintained if the sort order is changed.

Comments

-1

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)]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.