LoginSignup
1
0

SwiftDataでネストした子ModelのArrayは順番がぐちゃぐちゃになる。

Last updated at Posted at 2024-02-19

検証

検証用コード (SwiftDataを有効化したプロジェクトの初期コードにちょっと追加。差分はおおよその目安)
App.swift
import SwiftUI
import SwiftData

@main
struct testSwiftDataApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}
import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
+                       ForEach(item.notes) { note in
+                           Text(note.value)
+                       }
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(
                timestamp: Date(),
+                notes: [
+                    Note(value: "A"),
+                    Note(value: "B"),
+                    Note(value: "C"),
+                ]
            )
            modelContext.insert(newItem)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}
import Foundation
import SwiftData

@Model
final class Item {
    var timestamp: Date
+    var notes: [Note]

    init(timestamp: Date, notes: [Note]) {
        self.timestamp = timestamp
+        self.notes = notes
    }
}

+ @Model
+ final class Note {
+     var value: String
+ 
+     init(value: String) {
+         self.value = value
+     }
+ }

検証結果

List Detail 0 Detail 1 Detail 2
image.png Simulator Screenshot - iPhone 15 - 2024-02-19 at 12.55.18.png Simulator Screenshot - iPhone 15 - 2024-02-19 at 12.55.26.png Simulator Screenshot - iPhone 15 - 2024-02-19 at 12.55.33.png

順番ぐちゃぐちゃやないかい!

仮説:SwiftDataにおいて[子Model]の配列は順番がぐちゃぐちゃになる?

証跡
        .onAppear {
            for item in items {
                print(item._notes.map {$0.value})
            }
        }
        

として表示してみると

["A", "C", "B"]
["A", "B", "C"]
["B", "C", "A"]
["A", "C", "B"]
["C", "A", "B"]
["A", "C", "B"]
["A", "B", "C"]
["B", "A", "C"]
["C", "B", "A"]
...

となった。 確かにぐちゃぐちゃ。

対策

A. 子ModelをModelじゃなくする。

そもそも上記コードにおいて、Note@Modelを適用しPersistentModelに準拠させる必要がない。
なので、以下のように変更する。

Note.swift
struct Note { // Swiftの値型中心の思想に従い、不可欠でなければclassではなくstructを選ぶ。
    var value: String
    let id: UUID
    init(value: String) {
        self.value = value
        self.id = UUID()
    }
}

extension Note: Identifiable {} // ForEachで回すために準拠必要。

extension Note: Codable {} // SwiftDataで独自型を永続化するには、その型はCodableである必要がある。 

その他コードは変更なし。
Codableに関してはこちらの記事が参考になる

B. 回り道(Work Around)をする。

子Modelの配列および配列の順番が必要な場合、以下のようにすると意図した通りに動作した。

@Model
final class Item {
    var timestamp: Date
    private var _notes: [Note] // [Note]を保持する。
    private var idsInOrder: [UUID] // 順番をidで保持する。
    // [some PersistantModel]でない普通の配列の順番は保証される(仮説)

    var notes: [Note] { // 外部に露出するnotes
        get { // 保持者の値をidsInOrderでsortしてget
            var arr = [Note]()
            var elesToCheck = _notes

            for id in idsInOrder {
                guard let idx = elesToCheck.firstIndex(where: {$0.id ==  id}) else { continue }
                arr.append(elesToCheck[idx])
                elesToCheck.remove(at: idx)
            }
            return arr
        }
        set { // [Note]を保持者に、それらidをidsInOrderに与える。
            idsInOrder = newValue.map { $0.id }
            _notes = newValue
        }
    }

    init(timestamp: Date, notes: [Note]) {
        self.timestamp = timestamp
        self.idsInOrder = notes.map { $0.id }
        self._notes = notes

    }
}

@Model
final class Note: Identifiable {
    var value: String
    var id = UUID() // idを保持
    init(value: String) {
        self.value = value
    }
}

PersistentModel準拠型はIdentifierbleであり、PersistentIdentifier.IDの活用を考えたが、PersistentIdentifier.IDPersistentModelに準拠していないためできない旨のエラーが出た。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0