検証
検証用コード
(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 |
---|---|---|---|
順番ぐちゃぐちゃやないかい!
仮説: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.ID
はPersistentModel
に準拠していないためできない旨のエラーが出た。