Open4

SwiftData: 別のモデルを配列として持つとよく分からない動きになる件

kabeyakabeya

SwiftDataで、あるモデルに別のモデルのインスタンスを配列として持たせる、というようなことをします。

@Model
class ParentData {
    var name: String
    var timestamp: Date
    var children: [ChildData]
    
    init(name: String, timestamp: Date = Date.now, children: [ChildData]) {
        self.name = name
        self.timestamp = timestamp
        self.children = children
    }
}

@Model
class ChildData {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

サンプル作るときにあまり考えずに、名前を「親子」にしてしまいましたが、記事を書く段階で別に親子じゃないなと思ったりしますが、ともかくParentDataChildDataを配列として知っている、というような感じです。

これを、ビューに表示する際、ForEachで表示します。ParentDataChildDataForEachで回します。

struct ContentView: View {
    @Query(sort: [
        SortDescriptor(\ParentData.timestamp),
        SortDescriptor(\ParentData.name)]) var parents: [ParentData]
    @Environment(\.modelContext) var modelContext
    
    var body: some View {
        VStack {
            List {
                ForEach(parents) { parent in
                    VStack {
                        HStack {
                            let date = parent.timestamp.formatted(
                                Date.FormatStyle(date: .numeric, time: .shortened)
                                    .second(.twoDigits))
                            Text("\(parent.name)@\(date)")
                                .font(.headline)
                            Spacer()
                        }
                        .padding(8)
                        .background(Color(white: 0.8))
                        ForEach(parent.children) { child in
                            HStack {
                                Spacer()
                                Text(child.name)
                                    .padding(.leading, 24)
                            }
                        }
                    }
                }
            }
            .listStyle(.plain)
            Divider()
            Button("Add") {
                addParent()
            }
            .buttonStyle(.borderedProminent)
        }
    }
    
    private func addParent() {
        do {
            let childA = ChildData(name: "childA")
            let childB = ChildData(name: "childB")
            let parentX = ParentData(name: "parentX", children: [childA, childB])
            let parentY = ParentData(name: "parentY", children: [childB])
            
            modelContext.insert(childA)
            modelContext.insert(childB)
            modelContext.insert(parentX)
            modelContext.insert(parentY)
            try modelContext.save()
        }
        catch {
            print("error: \(error.localizedDescription)")
        }
    }
}

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [ParentData.self, ChildData.self])
        }
    }
}

「Add」ボタンを押すと、childA、childB、parentX、parentYが作られます。
問題は以下の2行です。

            let parentX = ParentData(name: "parentX", children: [childA, childB])
            let parentY = ParentData(name: "parentY", children: [childB])

parentXは、childAとchildB、parentYはchildBだけを持つというような初期化なんですよね。
(持つ、というのとは違うのかも知れませんが。関連を保持する、ですかね)

ですが実際に実行してみると、期待通りの結果にはなりません。

21:44:49に追加したparentX,Yは、childA、Bを1個ずつ持っています。
21:44:51に追加したparentX,Yは、XがchildA、Bを両方持ち、Yは何も持ちません。
21:50:01、21:50:06もこの繰り返しのようですね。

繰り返すたびにBがX,Yの間をフラフラしますが、XがA,B両方持ったうえでYがBを持つ、というのは何度繰り返しても現れません。
A,Bは一回ずつしか現れないんですね。

kabeyakabeya

ちなみに以下のように書くと、期待通りに表示されます。

private func addParent() {
        do {
            let childA = ChildData(name: "childA")
            let childB = ChildData(name: "childB")
            let childB2 = ChildData(name: "childB")
            let parentX = ParentData(name: "parentX", children: [childA, childB])
            let parentY = ParentData(name: "parentY", children: [childB2])
            
            modelContext.insert(childA)
            modelContext.insert(childB)
            modelContext.insert(childB2)
            modelContext.insert(parentX)
            modelContext.insert(parentY)
            try modelContext.save()
        }
        catch {
            print("error: \(error.localizedDescription)")
        }
    }
kabeyakabeya

結局、SwiftDataでは暗黙の関連は、1:1もしくは1:Nしか定義されない、ということですね。たぶん。

多対多の関連を定義する場合、子の側に、親への配列を持たせる必要がある、ということですね。おそらく。
以下の記事の「N : N のリレーションを定義する」「Explicit relationship(明示的なリレーション)」の部分。

https://zenn.dev/yumemi_inc/articles/2a929c839b2000#explicit-relationship(明示的なリレーション)-2

分かるけど、分かりたくない…
面倒くさい…