---
# System prepended metadata

title: SwiftUI CoreData
tags: [SwiftUI]

---

---
title: 'SwiftUI CoreData'
tags: SwiftUI
disqus: hackmd
---

**目錄：**

[TOC]

## 設定

當您創建 Xcode 專案時，不要選"use coreData"框，因為雖然它消除了一些無聊的設定程式碼，但它還添加了一大堆額外的範例程式碼，這些程式碼毫無意義，只是需要被刪除。

因此，您將學習如何手動設定核心數據。它需要三個步驟，從我們定義要在應用程式中使用的資料開始。

### 建立 Bookworm.xcdatamodeld
我們建立一個：按 Cmd+N 建立一個新文件，從模板清單中選擇資料模型，然後將模型命名為 Bookworm.xcdatamodeld。

![截圖 2025-01-06 下午5.09.13](https://hackmd.io/_uploads/Skip6GF8Jg.png)

設定要儲存的參數

![image](https://hackmd.io/_uploads/r1JlJQFLke.png)

### 建立 DataComtroller

建立一個名為 DataController.swift 的新 Swift 文件，並將其新增到其行上方import Foundation：

```swift=
import CoreData
```

創建一個 ObservableObject

在這個類別中，我們將新增一個 type 屬性NSPersistentContainer，它是核心資料類型，負責載入資料模型並讓我們存取內部資料。

```swift=
class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "Bookworm")
}
```

這告訴 Core Data 我們想要使用 Bookworm 資料模型。
這邊只是單純宣告，它實際上並沒有加載

要實際載入資料模型，我們需要呼叫 loadPersistentStores() 容器，它告訴 Core Data 根據 Bookworm.xcdatamodeld 中的資料模型存取我們保存的資料。
這不會同時將所有資料載入到記憶體中，因為這會造成浪費，但至少 Core Data 可以看到我們擁有的所有資訊。


```swift=
init() {
    container.loadPersistentStores { description, error in
        if let error = error {
            print("Core Data failed to load: \(error.localizedDescription)")
        }
    }
}
```

這段完成了 DataController 的實作，接下來的最後一步是創建一個 DataController 的實例，並將其傳入 SwiftUI 的環境中。

因為大多數應用程式通常只會使用一個 Core Data 存儲。
與其讓每個視圖各自創建自己的存儲，不如在應用啟動時只創建一次，然後將其存放在 SwiftUI 的環境中，這樣應用的其他部分就能方便地使用它了。

要完成這個操作，請打開 App.swift，並在結構體中新增以下屬性：

```swift=
@StateObject private var dataController = DataController()

WindowGroup {
    ContentView()
        .environment(\.managedObjectContext, dataController.container.viewContext)
}
```

## 讀取、寫入、刪除與更新

SwiftUI 提供了一個屬性包裝器（property wrapper），稱為 @FetchRequest，至少需要一個參數來描述結果的排序方式，它的格式比較特定。

```swift=
@FetchRequest(sortDescriptors: []) var students: FetchedResults<Student>
```
這段代碼創建了一個沒有排序的提取請求，並將其放入名為 students 的屬性中，該屬性的類型是 FetchedResults<Student>。
    
讀取：
```swift=
VStack {
    List(students) { student in
        Text(student.name ?? "Unknown")
    }
}
```
    
::: info 
student.name 是一個可選值，它可能有值，也可能沒有值。
這是 Core Data 中一個讓人頗感煩惱的地方之一，它有可選數據的概念，但這與 Swift 的可選值概念是完全不同的。
如果我們告訴 Core Data「這個屬性不能是可選的」（你可以在模型編輯器中設定），它仍然會生成可選的 Swift 屬性，因為對於 Core Data 來說，只要在保存時這些屬性有值就可以，其他時候它們可以是 nil。
:::

寫入、更新：
    
- 寫入與更新一致
    
```swift=
// 利用環境變數中的 managedObjectContext 來保存數據
@Environment(\.managedObjectContext) var moc
```

```swift=
let student = Student(context: moc) // 創建一個新的 Student 對象
student.id = UUID()
student.name = "\(firstNames.randomElement() ?? "Unknown") \(lastNames.randomElement() ?? "Unknown")"

try? moc.save() // 保存變更到托管上下文
```
    
刪除：

- 單一刪除
    
```swift=
if let student = students.first {
    moc.delete(student)
    try? moc.save() // 記得要保存，否則不會更新DB
}
```
    
- 全部刪除
    
```swift=
private func deleteAllData() {
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Student")
    let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    do {
        try context.executeAndMergeChanges(using: batchDeleteRequest)
        // 重置緩存，避免讀取到舊數據
        context.reset()
    } catch { // handle error here
        print("BATCH DELETE FAILED")
    }

    fetchStudents()
}
    
extension NSManagedObjectContext {
    /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
    ///
    /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
    /// - Throws: An error if anything went wrong executing the batch deletion.
    public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
        batchDeleteRequest.resultType = .resultTypeObjectIDs
        let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult // 執行刪除
        let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self]) // 合併變更
    }
}
```
    
| 步驟 | 目的 |
| -------- | -------- |
| NSBatchDeleteRequest | 直接刪除 Persistent Store 中的資料 |
| execute(batchDeleteRequest) | 執行批次刪除 |
| resultType = .resultTypeObjectIDs | 取得刪除的 NSManagedObjectID |
| mergeChanges(fromRemoteContextSave:) | 通知 NSManagedObjectContext 更新變更 |
| context.reset() | 清除快取，確保 UI 不會讀取到已刪除的資料 |
| fetchStudents() | 重新載入資料，確保 UI 立即更新 |

## 如何限制取得請求中的項目數量

```swift=
@FetchRequest(fetchRequest: fetchRequestLimit10()) var students: FetchedResults<Student>

static func fetchRequestLimit10() -> NSFetchRequest<Student> {
    let request: NSFetchRequest<Student> = Student.fetchRequest()
    request.fetchLimit = 10
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Student.name, ascending: true)]

    return request
}
```

:::danger
特別要注意的是，FetchedResultsController是依賴普通的NSFetchRequest的，而這個request至少需要指定一種sort，否則就會報一個錯誤：
:::

```swift=
'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
```

## 關聯

以下是幾種常見的資料庫關聯關係：
一對一（One-to-One）關聯：
    
在一對一關聯中，一個表的一筆記錄只與另一個表的一筆記錄關聯。
這種關聯適用於將兩個獨立的實體資訊分開存儲，但有時需要將它們連結在一起。
如前言所述，一個「使用者」表可以與一個「使用者主頁」表關聯，使每個使用者記錄關聯一個唯一的使用者主頁記錄。
    
一對多（One-to-Many）關聯：
    
在一對多關聯中，一個表格的一筆記錄可以與另一個表格的多筆記錄關聯。
這是最常見的關聯類型之一。
如前言所述，一個「使用者」表可以與一個「訂單」表關聯，一個使用者可以下多個訂單。
    
多對多（Many-to-Many）關聯：
    
多對多關聯表示兩個表中的多個記錄可以相互關聯。
為了實現這種關聯，通常需要一個中間表來連接這兩個表。
一個「使用者」表和一個「角色」表可以透過一個「使用者角色」表來實現多對多關聯，表示一個使用者可以兼任多個角色，一個角色可以賦予多個使用者。
    
關聯關係使資料庫能夠更好地管理資料之間的複雜關係，同時也能夠提高查詢的效率和資料的一致性。在設計資料庫時，正確使用關聯關係是十分重要的，這有助於保持資料的完整性和減少資料冗餘。
    
### 一對一


    建立實體
    
建立 User 使用者實體
    
![image](https://hackmd.io/_uploads/Bynbs8qL1x.png)

建立 Page 主頁實體
    
![image](https://hackmd.io/_uploads/HkMvnI5Ukl.png)

回到 User 配置關聯
    
![image](https://hackmd.io/_uploads/Sy6epIqL1x.png)

配置說明：
    
Relationship 為 page，表示指定關聯關係的名稱為 page，這個關係名稱可以隨意取，但最好得有意義，一眼能看出這個關係代表什麼，這裡用page表示用戶的主頁
    
Destination 為 Page，表示 User 實體的關聯對方為 Page 實體
    
右側面板中的Type則很重要，決定實體的關聯關係，這裡我們選擇的是（To One）即一對一
    
    一對一的資料操作
    
```swift=
@Environment(\.managedObjectContext) var viewContext
    
// 用户列表
@FetchRequest(sortDescriptors: [
    NSSortDescriptor(keyPath: \User.timestamp, ascending: true)
]) var users: FetchedResults<User>
```
    
讀取
    
```swift=
let title = user.page?.title ?? ""
```
    
新增
    
```swift=
let user = User(context: viewContext)
let page = Page(context: viewContext)
user.name = "小明"
user.score = 50
user.timestamp = Date()

page.title = "小明的主頁"
page.content = "主頁展示的内容"

user.page = page // 注意這邊
    
try? viewContext.save()
```
    
### 反向一對一
    
一對一和反向一對一隻是站在不同的角度上看待事物的關係，如果我們以User為主，正向一對一即 User 關聯 Page，在這個關係中我們說 Page 則是 User 的反向一對一關係。

我們進入Core Data模型檔案中，配置反向關聯關係
    
![image](https://hackmd.io/_uploads/B1FgZDc8Je.png)

這裡我們是以 Page 實體為主題，要反向關聯的是用戶，等於是我們需要在 Page 的實例中，添加一個用戶的實例信息，而第二步關聯關係中的 Inverse 則要指定 User 關聯關係的 page 來建立反向關聯關係
    
```swift=
@FetchRequest(sortDescriptors: [
        NSSortDescriptor(keyPath: \Page.title, ascending: true)
]) var pages: FetchedResults<Page>
```
   
讀取

```swift=
let pageName = page.user?.name ?? ""
```
    
新增(其實就是倒過來換成 pencil 塞 student 而已)
 
```swift=
let user = User(context: viewContext)
page.user = user // 注意這邊
    
try? viewContext.save()
```
    
### 一對多

建立 Order 實體

![image](https://hackmd.io/_uploads/r1I0nP9U1x.png)

配置User實體與Order實體的關聯關係
    
![image](https://hackmd.io/_uploads/HJE1awqUJx.png)

上圖步驟一中，我們配置 Relationship 中的關係名稱為 orders，名稱採用複數寓意這個關係屬性下有多個訂單。
步驟二 Type 類型則也要選擇 To Many
    
    一對多數據的操作
    
```swift=
@Environment(\.managedObjectContext) var viewContext
    
// 用户列表
@FetchRequest(sortDescriptors: [
    NSSortDescriptor(keyPath: \User.timestamp, ascending: true)
]) var users: FetchedResults<User>
```

讀取

```swift=
if let orders = user.orders?.allObjects as? [Order] {
    print(orders)
}
```

新增

```swift=
let user = User(context: viewContext)
user.name = "小明"
user.score = 50
user.timestamp = Date()

let order1 = Order(context: viewContext)
order1.no = UUID()
order1.total = 100

let order2 = Order(context: viewContext)
order2.no = UUID()
order2.total = 200
user.orders = [order1, order2]

try? viewContext.save()
```

新增邏輯與一對一唯一不同點在於 user.page 接受一個 Page 對象，而user.orders 接受的是一個數組，我們只需要實例多個Order並拼裝成一個數組傳入即可。
渲染邏輯則是透過 User 實例的關聯關係屬 user.orders?.allObjects 拿到其所有關聯物件再循環渲染
    
### 多對多
    
用戶與用戶角色我們認為可以是多對多的關係，一個用戶可以兼任多個角色，一個角色可以賦予多個用戶
    
建立 Role

![image](https://hackmd.io/_uploads/SJnc4_981l.png)

在 User 使用者實體中配置與 Role 角色實體的關係

![image](https://hackmd.io/_uploads/SklnEOcUkx.png)

在 Role 角色實體中設定與 User 使用者實體的關係
    
![image](https://hackmd.io/_uploads/SJPMBd58Jl.png)
    
多對多的概念是站在上帝視角去看使用者和角色的關係，但站在其中任意一方的視角上雙方都互為對方的一對多。
在實際的應用中，同樣我們只會以一方為主體去關聯查詢另一方的數據，所以配置時在雙方的Type都是選擇To Many(一對多)
    
當維度降到一對多時，程式碼和資料操作就跟上一小節的一對多毫無差別，因此這邊讀取與新增都跟上面沒差別

## 版本化與遷移
    
輕量級遷移的運作

輕量級遷移可以自動處理：
	•	新增或刪除表。
	•	在現有表中新增屬性（欄位）。
	•	將屬性設置為可選或添加默認值。
	•	基本的屬性類型轉換（例如從 Int 到 Double）。
    

那為何需要手動遷移呢

即使輕量級遷移處理了大部分場景，也有一些變更可能會導致問題，例如：
1.	刪除屬性或表
    如果刪除了某個屬性或表，Core Data 需要知道如何處理已有的數據，這可能需要手動遷移。
2.	將欄位從可選改為必填
    這需要為所有現有數據提供默認值，否則可能導致遷移失敗。
3.	改變欄位的類型或關聯結構
    例如，將一對多的關聯改為多對多，可能需要自定義遷移。

### 1.添加新版本

在 Xcode 中，創建新版本的操作如下：
1.	打開 .xcdatamodeld 文件。
2.	點擊左下角的齒輪按鈕，選擇 Add Model Version。
3.	給新版本命名，例如 MyModel 2。
4.	確保新版本的基礎是現有版本（它會自動複製現有結構）。
5.	編輯新版本的結構（例如新增表、新增欄位等）。
    
![截圖 2025-01-10 下午2.56.24](https://hackmd.io/_uploads/HyIySHR8kg.png)

    
### 2.設置當前版本

完成新版本的編輯後：
1.	選擇 .xcdatamodeld 文件。
2.	在右側的 Model Version 區域中，將新版本設置為 Current。
    
![截圖 2025-01-10 下午2.58.06](https://hackmd.io/_uploads/SygHHHAIyx.png)

這樣，Xcode 會將新版本設為默認模型版本，應用啟動時將使用它。
