🗂

Notionのようなデータ構造をSwiftDataで作成する

2024/01/08に公開

Notionのような階層構造のブロックを内部で持つデータ構造をSwiftDataを使って表現します

(こういうやつ)

これをNavigationStackにマッピングして閲覧できるようにします

モデル: Item.swift

@Relationshipを使って自己参照リレーションを張ります

import Foundation
import SwiftData

@Model
final class Item {
    var timestamp: Date
    var content: String
    var parent: Item?

    init(content: String, parent: Item? = nil, timestamp: Date? = nil) {
        self.content = content
        self.parent = parent
        self.timestamp = timestamp ?? Date()
    }
    
    @Relationship(deleteRule: .cascade, inverse: \Item.parent)
    var children: [Item] = [Item]()
}

Top: ContentView.swift

親を持たないルートブロックを描画するView

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    @Query(filter: #Predicate<Item> { item in
        item.parent == nil
    }, sort: \.timestamp, order: .forward)
    private var items: [Item]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        ContentDetailView(item: item)
                    } label: {
                        Text(item.content)
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            .navigationTitle("Top")
        }
    }    
}

オプション: addItem()

テストデータをインサートするボタンを実装します

    private func addItem() {
        withAnimation {
            let newItem = Item(content: "Parent")
            let child = Item(content: "Child", parent: newItem)
            let grandchild = Item(content: "Grandchild", parent: child)
            
            modelContext.insert(newItem)
            modelContext.insert(child)
            modelContext.insert(grandchild)
            modelContext.insert(Item(content: "Grandchild 2", parent: child))
            modelContext.insert(Item(content: "Grandchild 3", parent: child))
        }
        
    }

オプション: deleteItems()

削除ボタンのハンドリング実装

@Relationship(deleteRule: .cascade, inverse: \Item.parent)を設定しているので下位のブロックがまとめて削除されます

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

下位ブロック: ContentDetailView.swift

import SwiftUI
import SwiftData

struct ContentDetailView: View {
    var item: Item
    
    var body: some View {
        List {
            ForEach(item.children) { item in
                NavigationLink {
                    ContentDetailView(item: item)
                } label: {
                    Text(item.content)
                }
            }
        }.navigationTitle(item.content)
    }
}

SQLiteの内部データをデバッグする

SwiftDataはCore Dataスキーマを継承していて、シミュレータの場合は以下にSQLiteファイルがあります

find ~/Library/Developer/CoreSimulator/Devices -name default.store
sqlite3 ~/Library/Developer/CoreSimulator/Devices/**/data/Containers/Data/Application/**/Library/Application Support/default.store
sqlite> .tables
ACHANGE             ATRANSACTIONSTRING  Z_METADATA          Z_PRIMARYKEY      
ATRANSACTION        ZITEM               Z_MODELCACHE   

sqlite> SELECT * FROM  "ZITEM" ORDER BY "Z_PK" LIMIT 300 OFFSET 0;
6|1|1||726376858.878182|Parent
7|1|1|8|726376858.879967|Grandchild 2
8|1|1|6|726376858.878591|Child
9|1|1|8|726376858.87864|Grandchild
10|1|1|8|726376858.880036|Grandchild 3
11|1|1||726376859.655562|Parent
12|1|1|15|726376859.656266|Grandchild 2
13|1|1|15|726376859.656499|Grandchild 3
14|1|1|15|726376859.655821|Grandchild
15|1|1|11|726376859.6557|Child
16|1|1|17|726376860.290472|Grandchild 2
17|1|1|18|726376860.289769|Child
18|1|1||726376860.289574|Parent
19|1|1|17|726376860.289951|Grandchild
20|1|1|17|726376860.290769|Grandchild 3

Discussion