💊

SwiftDataで 1:N のときに配列を使うが、配列に見えて実は順番を保持しない

2024/10/12に公開

環境

Sequoia 15.0.1
Xcode 16.0
動作確認はシミュレータ iPhone 16 iOS18.0

問題

SwiftDataで1:Nのデータベースを作る時に次のように配列を使うと思う

@Model
class A {
    @Attribute(.unique)
    var id = UUID().uuidString
    var name: String
    @Relationship(deleteRule: .cascade)
    var b: [B] = [] //⭐️ここで相手のBを保持するのに配列を使っている
    init(name: String) {
        self.name = name
    }
}

@Model
class B {
    @Attribute(.unique)
    var id = UUID().uuidString
    var name: String
    @Relationship(deleteRule: .nullify, inverse: \A.b)
    var a: A?
    init(name: String) {
        self.name = name
    }
}

しかし次の部分で aa.b から取り出される順番は aa.b に追加した順番ではない。一度アプリを閉じてから上げ直すと現象がよく出る。

@Query var a: [A]
if let aa = a.first {
    ForEach(aa.b) {
        Text("\($0.name)") //⭐️バラバラな順番
    }
}

対策

私の場合、幸いBの中に表示順を設定する order を入れても(あるAから独り占めされても)大丈夫な設計だったので、Bに order を入れ、表示する際はこの order によって並べ替えるようにする。

並べ替えは、

  • 自分で表示用のデータを作成して並べ替える
  • SwiftDataのQueryに任せる

があると思うがSwiftDataに任せる方にしてみた。

@Model
class B {
    @Attribute(.unique)
    var id = UUID().uuidString
    var name: String
    @Relationship(deleteRule: .nullify, inverse: \A.b)
    var a: A?
    var order: Int = 0 //⭐️orderを追加
    init(name: String) {
        self.name = name
    }
}

この order は配列に追加する時に設定したり、ユーザーの並べ替え操作などで設定されるもの。

//ContentViewのbody内で
if let aa = a.first {
    ContentView2(id: aa.id)
}

ContentView2 に切り出した。 ContentViewbody から呼び出して ContentView2init でQueryを確定させる。
これは単に、Queryを動的に変化させるやり方がわからなかったので。

struct ContentView2: View {
    
    @Environment(\.modelContext) var modelContext
    @Query var displayedB: [B]
    
    init(id: String) {
        _displayedB = Query(filter: #Predicate<B> { $0.a?.id == id }, sort: \B.order)
    }
    
    var body: some View {
        Section {
            ForEach(displayedB) {
                Text("\($0.name)")
            }
        }
    }
}

自分で表示用のデータを作成して並べ替えるのに比べて、このやり方はQueryの処理が余計に走ると思うので、そこには注意したい。

上がばらばら
下がそろっている様子

Discussion