精通 SwiftUI - iOS 17 版

第 22 章
如何使用 SwiftData 建立 ToDo App

iOS App 開發的一個常見問題是我們如何使用 Core Data 和 SwiftUI 將數據永久保存在內置數據庫中。 在本章中,我們將通過構建一個 ToDo 應用程序來回答這個問題。

由於 ToDo 這個範例程式使用 List 和 Combine 來處理數據展示和共享,我假設你已經閱讀了以下章節:

  • 第 7 章 - 狀態(State)與綁定(Binding)
  • 第 10 章 - 動態列表、 ForEach 與 Identifiable 的使用方法
  • 第 14 章 - 使用 Combine 與 Environment 物件進行資料分享

如果你還未閱讀或忘記了Combine 和Environment Objects 是什麼,請先閱讀這些章節。

究竟你會將會做什麼以理解 SwiftData? 我已經構建了範例App的核心部分,你並不需要從頭開始構建 ToDo App。 但是,它不能永久保存數據。 更具體地說,它只能將待辦事項保存在記憶體當中。 每當用戶關閉App並再次啟動它時,所有數據都會消失。 我們將修改這個範例 App 並將其轉換為使用 SwiftData 將數據永久保存到本地數據庫。 圖 22.1 顯示了 ToDo App 的一些示例屏幕截圖。

圖 22.1. ToDo app
圖 22.1. ToDo app

在我們開始修改之前,我會先介紹一下這個 Starter 項目,以便你完全理解當中的程式碼。

除了 SwiftData,你還將會學習如何自定義切換的樣式。 看看上面的截圖,該核取方塊實際上是 SwiftUI 的切換視圖。 我將向你展示如何透過自定義 Toggle 的樣式來建立這些核取方塊。

本章有很多內容要講,讓我們開始吧!

SwiftData 介紹

在我們查看 ToDo App的Starter項目之前,讓我向你簡單介紹一下 SwiftData 以及你將如何在 SwiftUI 項目中使用它。

首先,需要注意的是,SwiftData 框架不應與資料庫混淆。 SwiftData 建構在 Core Data 之上,實際上是一個框架,旨在幫助開發人員管理持久性儲存上的資料並與之互動。 雖然 iOS 的預設持久性儲存通常是 SQLite 資料庫,但值得注意的是,持久性儲存也可以採用其他形式。 例如,Core Data 也可以用於管理本機檔案中的數據,例如 XML 檔案。

無論你使用的是 Core Data 還是 SwiftData 框架,這兩種工具都可以幫助開發人員免受底層持久儲存的複雜性的影響。 以 SQLite 資料庫為例。 使用 SwiftData,無需擔心連接資料庫或理解 SQL 來檢索資料記錄。 相反,開發人員可以專注於使用 API 和 Swift 巨集(例如 @Query@Model)來有效管理應用程式中的資料。

iOS 17 中新引入了 SwiftData 框架,以取代先前稱為 Core Data 的框架。 自 Objective-C 時代以來,Core Data 一直是 iOS 開發的資料管理 API。 儘管開發人員可以將該框架整合到 Swift 專案中,但 Core Data 並不是 Swift 和 SwiftUI 的本機解決方案。

在最新版的 iOS 中,Apple 終於為 Swift 引入了一個名為 SwiftData 的原生框架,用於持久資料管理和資料建模。 它建立在 Core Data 之上,但 API 完全重新設計以充分利用 Swift。

圖 22.2. 資料模型編輯器
圖 22.2. 資料模型編輯器

如果你曾使用過 Core Data,可能會記得必須使用資料模型編輯器建立資料模型(檔案副檔名為 .xcdatamodeld)以實現資料持久性。 隨著 SwiftData 的發布,你不再需要這樣做。 SwiftData 使用巨集(這是 iOS 17 中的另一個新 Swift 功能)簡化了整個過程。例如,你已經為 Song 定義了一個模型類,如下所示:

class Song {
  var title: String
  var artist: String
  var album: String
  var genre: String
  var rating: Double
}

若要使用 SwiftData,新的 @Model 巨集是使用 SwiftUI 儲存持久資料的關鍵。 SwiftData 不需要使用模型編輯器建立資料模型,而是只需要使用 @Model 巨集註解模型類,如下所示:

@Model class Song {
  var title: String
  var artist: String
  var album: String
  var genre: String
  var rating: Double
}

這就是在程式碼中定義資料模型架構的方式。 透過這個簡單的關鍵字,SwiftData 會自動啟用資料類別的持久性,並提供其他資料管理功能,例如 iCloud 同步。 屬性是從屬性推斷出來的,它支援基本值類型,例如 Int 和 String。

SwiftData 允許您使用屬性元資料自訂架構的建置方式。 你可以使用 @Attribute 註解新增唯一性約束,並使用 @Relationship 註解刪除傳播規則。 如果你不想包含某些屬性,可以使用 @Transient 巨集告訴 SwiftData 排除它們。 這是一個例子:

@Model class Album {
  @Attribute(.unique) var name: String
  var artist: String
  var genre: String

  // The cascade relationship instructs SwiftData to delete all 
    // songs when the album is deleted.
  @Attribute(.cascade) var songs: [Song]? = []
}

為了驅動資料持久化操作,你需要熟悉 SwiftData 的兩個關鍵物件:「ModelContainer」和「ModelContext」。 「ModelContainer」充當模型類型的持久後端。 要建立 ModelContainer,你只需實例化它的一個實例。

// Basic
let container = try ModelContainer(for: [Song.self, Album.self])

// With configuration
let container = try ModelContainer(for: [Song.self, Album.self], 
                                    configurations: ModelConfiguration(url: URL("path"))))

在 SwiftUI 中,你可以在應用程式的根目錄中設定模型容器:

import SwiftData
import SwiftUI

@main
struct MusicApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer (for: [Song.self, Album.self]))
    }
}

設定模型容器後,就可以開始使用「model context」來取得和儲存資料。 「model context」可用作追蹤更新、獲取資料、保存變更甚至撤消這些更改的介面。 使用 SwiftUI 時,通常可以從視圖環境中取得「model context」:

struct ContextView: View {
    @Environment(\.modelContext) private var modelContext
}

有了「model context」,你就可以獲得數據了。 最簡單的方法是使用 @Query 屬性包裝器。 您可以使用一行程式碼輕鬆載入和過濾資料庫中儲存的任何內容。

@Query(sort: \.artist, order: .reverse) var songs: [Song]

若要在持久性儲存中插入項目,你可以呼叫「model context」的 insert 方法並向其傳遞要插入的模型物件。

modelContext.insert(song)

同樣,你可以透過「model context」刪除該項目,如下所示:

modelContext.delete(song)

這是 SwiftData 的簡單介紹。 如果你仍然對如何使用 SwiftData 感到困惑? 不用擔心。 建立 ToDO 應用程式後你將了解其用法。

了解 ToDo App 範例運作

現在你對 SwiftData 有了基本的了解,讓我和你一起完成這個範例App。 稍後,我們將修改此 ToDo App,讓它能永久保存待辦事項。 現在,如前所述,所有數據都存儲在記憶體當中,並且會在App重新啟動時消失。

首先,請從 https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListStarter.zip 下載啟動項目。 解壓檔案並在 Xcode 中打開 ToDoList.xcodeproj。 選擇 ContentView.swift 並預覽 UI。 你應該會看到如圖 22.3 所示的屏幕。

圖 22.3. 預覽範例App
圖 22.3. 預覽範例App

在預覽框或模擬器中運行App。 按 + 並添加待辦事項。 之後,再重複添加更多項目。 然後App就會列出所有待辦事項。 在待辦事項的格子按一下,App就會劃掉該項目。

圖 22.4. 添加新事項
圖 22.4. 添加新事項

如何顯示待辦事項列表

先讓我們看一下程式碼,以便你了解這些程式碼背後的運作原理。 首先,我們從模型類開始。 在 Model 文件夾中打開 ToDoItem.swift

enum Priority: Int {
    case low = 0
    case normal = 1
    case high = 2
}

@Observable class ToDoItem: Identifiable {
    var id: UUID
    var name: String
    var priority: Priority
    var isComplete: Bool

    init(id: UUID = UUID(), name: String = "", priority: Priority = .normal, isComplete: Bool = false) {
        self.id = id
        self.name = name
        self.priority = priority
        self.isComplete = isComplete
    }
}

這個範例App是一個普通 ToDo App的簡化版本。 每個待辦事項(或任務)都有三個屬性:namepriorityisComplete(即任務的狀態)。 該類別具有@Observable巨集,這是iOS 17中的新功能。它為ToDoItem添加了觀察支持,並使其符合Observable協定。

使用該巨集,這三個屬性會自動標記為 @Published,以便每當值發生任何變更時都會通知訂閱者。 隨後,在 ContentView 的實作中,SwiftUI 監聽值的變更並相應地更新視圖。 例如,當「isComplete」的值變更時,它會切換該複選框。

這個類別也符合Identifiable 協議,這樣ToDoItem 的每個實體(instance)都有一個獨一無二的識別碼。 稍後,我們將使用 ForEachList 來顯示待辦事項。 這就是為什麼我們需要採用協議並創建 id 屬性。

現在讓我們轉到視圖並從ContentView.swift 開始。 假設你已經閱讀了第 10 章,你應該理解大部分程式碼。 內容視圖包含三個主要部分,它們嵌入在一個「ZStack」中:

  1. 顯示所有待辦事項的列表視圖。
  2. 沒有待辦事項時顯示的空視圖 (NoDataView)。
  3. 當用戶點擊 + 按鈕時顯示的「添加新事項」視圖。

先看第一個VStack

VStack {

    HStack {
        Text("ToDo List")
            .font(.system(size: 40, weight: .black, design: .rounded))

        Spacer()

        Button(action: {
            self.showNewTask = true    
        }) {
            Image(systemName: "plus.circle.fill")
                .font(.largeTitle)
                .foregroundStyle(.purple)
        }
    }
    .padding()

    List {        
        ForEach(todoItems) { todoItem in
            ToDoListRow(todoItem: todoItem)
        }                      
    }
    .listStyle(.plain)
}
.rotation3DEffect(Angle(degrees: showNewTask ? 5 : 0), axis: (x: 1, y: 0, z: 0))
.offset(y: showNewTask ? -50 : 0)
.animation(.easeOut, value: showNewTask)

我加了一個名為 todoItems 的變數來保存所有的待辦事項。 這個變數標有@State,以便在有任何更改時更新列表。 在 List 視圖中,我們使用 ForEach 以顯示所有項目。

另外,我們有一個名為ToDoListRow的獨立視圖以處理列表的行:

struct ToDoListRow: View {

    @Bindable var todoItem: ToDoItem

    var body: some View {
        Toggle(isOn: self.$todoItem.isComplete) {
            HStack {
                Text(self.todoItem.name)
                    .strikethrough(self.todoItem.isComplete, color: .black)
                    .bold()
                    .animation(.default)

                Spacer()

                Circle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(self.color(for: self.todoItem.priority))
            }
        }.toggleStyle(CheckboxStyle())
    }

    private func color(for priority: Priority) -> Color {
        switch priority {
        case .high: return .red
        case .normal: return .orange
        case .low: return .green
        }
    }
}

這個視圖接受一個待辦事項,它是一個ObservableObject。 這意味著該待辦事項有任何更改時,相關的視圖將自動更新UI。在 iOS 17 中,您可以使用 @Bindable 屬性包裝器來保存 obersvable 物件的綁定。

對於每一行的待辦事項,由三部分組成:

  1. 切換(Toggle)/ 核取方塊(Checkbox) - 提示事項是否完成。
  2. 文字標籤 - 顯示事項名稱
  3. 點/圓圈 - 顯示事項的優先次序

第二和第三部分非常簡單。 至於核取方塊,這個比較值得更深入的討論。 SwiftUI 提供有一個名為Toggle的標準控件。 在前面的章節中,我們用它來建立一個App設定畫要。 這裡的切換(Toggle)更像是一個開關,可讓你打開或關閉,但在 ToDo App中,我們想讓切換看起來像一個核取方塊。

自訂 Toggle 的外觀

與我們在第 6 章中討論的 Button 類似,Toggle 也允許開發人員自定義其樣式。 你需要做的就是實現ToggleStyle協議。 在項目導航器中,打開 CheckBoxStyle.swift 查看:

struct CheckboxStyle: ToggleStyle {

    func makeBody(configuration: Self.Configuration) -> some View {

        return HStack {

            Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
                .resizable()
                .frame(width: 24, height: 24)
                .foregroundColor(configuration.isOn ? .purple : .gray)
                .font(.system(size: 20, weight: .bold, design: .default))
                .onTapGesture {
                    configuration.isOn.toggle()
                }

            configuration.label

        }
    }
}

在程式碼中,我們實作了makeBody方法,這是協議的要求。 我們加入了一個圖像視圖,取決於切換的狀態(即configuration.isOn),它將顯示一個選框圖像或一個圓形圖像。 這就是如何自定義切換樣式的方法。

要使用 CheckboxStyle,請將 toggleStyle 修飾器附加到 Toggle 並指定核取方塊樣式,如下所示:

.toggleStyle(CheckboxStyle())

處理空列表視圖

當陣列中沒有項目時,我們顯示一個圖像視圖而不是一個空的列表視圖。 這個並不是必須做,但是,我認為它使App看起來更好,並讓使用者知道在App首次啟動時該做什麼。

// 如果沒有資料,就顯示一個空視圖
if todoItems.count == 0 {
    NoDataView()
}

由於我們有一個 ZStack 來嵌入視圖,所以很容易控制這個空視圖何時顯示。那就是當陣列為空時顯示出來。

顯示添加事項視圖

當使用者點擊右上角的 + 按鈕時,App 會顯示「NewToDoView」。 此視圖覆蓋在列表視圖的頂部,看起來像一個由畫面底彈出來的頁面。 我們還添加了一個空白視圖來使列表視圖變暗。

下面是程式碼供參考:

if showNewTask {
    BlankView(bgColor: .black)
        .opacity(0.5)
        .onTapGesture {
            self.showNewTask = false
        }

    NewToDoView(isShow: $showNewTask, todoItems: $todoItems, name: "", priority: .normal)
        .transition(.move(edge: .bottom))
        .animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0), value: showNewTask)
}

了解添加事項視圖

現在讓我和你討論一下 NewToDoView.swift 中的程式碼,該程式碼用於讓使用者添加新任務或待辦事項。 你可以參考圖 22.6 或簡單地打開檔案進行預覽,看看該視圖的外觀。

NewToDoView 接受兩個綁定:isShowtodoItemsisShow 參數控制這個 Add New Task 視圖是否應該出現在螢幕上。 todoItems 變數儲存了對待辦事項陣列的參考。 我們需要方法呼叫者將綁定傳遞給 todoItems,以便我們可以將新事項加進陣列。

@Binding var isShow: Bool
@Binding var todoItems: [ToDoItem]

@State var name: String
@State var priority: Priority
@State var isEditing = false

在視圖中,我們讓用戶輸入事項名稱並設置其優先次序(低/正常/高)。 狀態變數isEditing標示是否進入編輯模式。 為避免軟體鍵盤遮擋編輯畫面,App將在使用者編輯文字時稍為向上移動視圖。

TextField("Enter the task description", text: $name, onEditingChanged: { (editingChanged) in

    self.isEditing = editingChanged

})


...

.offset(y: isEditing ? -320 : 0)

點擊 Save 按鈕後,我們驗證文字欄是否有輸入文字。 如果使用者有輸入文字,我們會建立一個新的 ToDoItem 並呼叫 addTask 方法將其附加到 todoItems 陣列。相反,我們就什麼都不做了。

Button(action: {

    if self.name.trimmingCharacters(in: .whitespaces) == "" {
        return
    }

    self.isShow = false
    self.addTask(name: self.name, priority: self.priority)

}) {
    Text("Save")
        .font(.system(.headline, design: .rounded))
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
        .foregroundColor(.white)
        .background(Color.purple)
        .cornerRadius(10)
}
.padding(.bottom)

由於 todoItems 陣列是一個狀態變數,列表視圖會自動更新並顯示新事項。 這就是整個程式碼的運作原理。 如果你不明白 Add task 視圖是如何顯示在螢幕底部的話,請參閱第 18 章。

如何使用 SwiftData

現在我已經為你介紹了這個ToDo項目,是時候將App轉換為使用 SwiftData 將待辦事項存儲在數據庫中了。

使用 @Model 透過程式碼定義架構

如前所述,SwiftData 使得使用程式碼定義方案變得非常容易。 您所需要的只是使用 @Model 巨集註解模型類別。 現在打開 ToDoItem.swift 並新增以下語句以導入SwiftData框架:

import SwiftData

接下來,將 ToDoItem 類別的@Observable 更改為 @Model

@Model class ToDoItem: Identifiable {
    var id: UUID
    var name: String
    var priority: Priority
    var isComplete: Bool

    init(id: UUID = UUID(), name: String = "", priority: Priority = .normal, isComplete: Bool = false) {
        self.id = id
        self.name = name
        self.priority = priority
        self.isComplete = isComplete
    }
}

修改後,Xcode 會顯示許多編譯錯誤。 原因是「Priority」是一個枚舉,不能直接儲存在持久性儲存中。 為了將此枚舉保存到資料庫中,我們將儲存其原始值,該值是一個整數。 讓我們像這樣更改 ToDoItem 類別:

@Model class ToDoItem: Identifiable {
    var id: UUID
    var name: String

    @Transient var priority: Priority {
        get {
            return Priority(rawValue: Int(priorityNum)) ?? .normal
        }

        set {
            self.priorityNum = Int(newValue.rawValue)
        }
    }
    @Attribute(originalName: "priority") var priorityNum: Priority.RawValue

    var isComplete: Bool

    init(id: UUID = UUID(), name: String = "", priority: Priority = .normal, isComplete: Bool = false) {
        self.id = id
        self.name = name
        self.priority = priority
        self.isComplete = isComplete
    }
}

在上面的程式碼中,我們新增了一個名為「priorityNum」的新屬性,它與「Priority」枚舉的原始值(即「Int」)具有相同的類型。 此原始值將直接儲存在持久性儲存中。 對於「priorityNum」屬性,我們也指示 SwiftData 對底層架構使用不同的欄位名稱。

對於原始的 priority 屬性,我們已將其變更為計算屬性。 此計算屬性負責將優先權編號轉換為枚舉,反之亦然。 由於此計算屬性不需要儲存在持久性儲存中,因此我們使用 @Transient 巨集對其進行註釋。

更改後,您應該能夠在模擬器中建置並運行該應用程式。( 請注意,部分版本的 Xcode 15 將無法預覽 UI,但應該能夠在模擬器中建置並運行該應用程式。)

設定模型容器

現在打開 SwiftUIToDoListApp.swift 並為應用程式設定模型容器。 在 SwiftUIToDoListApp 結構,更新程式碼如下:

struct SwiftUIToDoListApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: ToDoItem.self)
    }
}

我們設定一個共享模型容器來儲存「ToDoItem」的實例。

將數據添加到持久存儲

設定模型容器後,我們就可以使用 Model Context 來取得和儲存待辦事項。讓我們先更新 NewToDoView.swift 的程式碼。 要在數據庫中存新事項,首先需要從環境中獲取 Model Context:

@Environment(\.modelContext) private var modelContext

由於我們不再使用陣列來保存待辦事項,你可以刪除這行程式碼:

@Binding var todoItems: [ToDoItem]

接下來,讓我們像這樣更新 addTask 函數:

private func addTask(name: String, priority: Priority, isComplete: Bool = false) {

    let task = ToDoItem(name: name, priority: priority, isComplete: isComplete)

    modelContext.insert(task)
}

要將新記錄加入數據庫,你需要使用託管物件內容建立一個 ToDoItem,然後呼叫 save() 方法來儲存更改。

由於我們移除了 todoItems 綁定,我們需要更新預覽程式碼:

#Preview {
    NewToDoView(isShow: .constant(true), name: "", priority: .normal)
}

現在讓我們回到ContentView.swift。 同樣地,你應該會在ContentView中看到一個錯誤(見圖 22.5)。

圖 22.5. Xcode 向你顯示 ContentView 中的錯誤
圖 22.5. Xcode 向你顯示 ContentView 中的錯誤

像這樣更改程式碼行以修復錯誤:

NewToDoView(isShow: $showNewTask, name: "", priority: .normal)

我們只需刪除 todoItems 參數。 這就是我們將範例App從使用陣列存儲轉換為持久存儲的方式。

現在您可以運行 App 並添加一些新任務。 它們應該永久保存到資料庫中。

使用@Query 取得記錄

我們已成功將資料新增至資料庫。 但是,待辦事項尚未顯示在主畫面上。 為了解決這個問題,我們需要對 ContentView.swift 進行一些更改。 首先,加入一條導入語句來導入SwiftData框架:

import SwiftData

接下來,我們有一個陣列變數來保存所有待辦事項,它也標有 @State

@State var todoItems: [ToDoItem] = []

由於我們要將項目儲存在資料庫中,因此我們需要修改這行程式碼並從中取得資料。 Apple 引進了一個名為@Query 的新屬性包裝器。 這使得從資料庫載入資料變得非常容易。

只需將 @State 替換為 @Query 並刪除預設值,如下所示:

@Query var todoItems: [ToDoItem]

這個 @Query 屬性會自動為您取得所需的資料。 在上面的程式碼中,我們指定要取得 ToDoItem 實例。 另外,您可以使用 sort 選項來配置屬性包裝器,並指定結果的排序方式。 以下定義將根據優先順序對項目進行排序:

@Query(sort: \ToDoItem.priorityNum, order: .reverse) var todoItems: [ToDoItem]

這是執行獲取請求並從資料庫檢索資料的方式。 而且,由於 ToDoItem 的屬性保持不變,我們不需要對清單視圖進行任何程式碼變更。 我們可以直接在ForEach中使用取得結果:

List {

    ForEach(todoItems) { todoItem in
        ToDoListRow(todoItem: todoItem)
    }

}

現在可以再次測試App! 啟動它後,就能夠看到您在上一節中創建的所有任務。

更新現有項目

SwiftData 簡化了處理持久性儲存的程序,所需要修改的程式碼也顯著減少。 只需使用 @Model 巨集標記模型對象,SwiftData 就會自動修改設定器以進行更改追蹤和觀察。 這意味著無需更改程式碼即可更新待辦事項。

現在你可以在模擬器中運行App進行測試, 你應該能夠添加新事項。 添加新事項後,它們應立即顯示在列表視圖中。 核取方塊也應該可以用了,點一下就可以將相關事項畫線。 最重要的是,所有改動現在都永久保存在裝置內的數據庫。 就算重新App,所有項目仍然存在。

圖 22.6. 更新現有項目
圖 22.6. 更新現有項目

從數據庫中刪除項目

既然我已經向你展示如何從數據庫取出數據、更新和加入新項目,那麼如何實作刪除功能呢? 我們將會為App添加一項功能 - 刪除待辦事項。

ContentView 結構中,宣告一個 modelContext 變數:

@Environment(\.modelContext) private var modelContext

然後添加一個名為deleteTask的函數,如下所示:

private func deleteTask(indexSet: IndexSet) {
    for index in indexSet {
        let itemToDelete = todoItems[index]
        modelContext.delete(itemToDelete)
    }
}

此函數接收一個索引集,該集儲存要刪除項目的索引。 要從數據庫中刪除項目,你可以呼叫modelContextdelete 方法並指定要刪除的項目。

既然我們已經準備好了刪除方法,那麼我們應該在哪裡呼叫它呢? 將 onDelete 修飾器附加到列表視圖的 ForEach,如下所示:

List {

    ForEach(todoItems) { todoItem in
        ToDoListRow(todoItem: todoItem)
    }
    .onDelete(perform: deleteTask)

}

onDelete 修飾器自動啟用列表視圖中的滑動刪除功能。 當用戶刪除一個項目時,我們調用deleteTask 方法從數據庫中刪除該項目。

執行App並試試刪除一個項目,按delete鈕就可以將其從數據庫中完全刪除。

圖 22.7. 刪除一個項目
圖 22.7. 刪除一個項目

使用 SwiftUI 預覧

不知你有沒有留意,現在Xcode內的SwiftUI預覽已經不能正常顯示預覽。 這是可以理解的,因為我們沒有在 #Preview 中注入 model container。 那麼,我們要如何解決問題並使預覽正常運作?

首先,我們需要建立一個數據暫存器(in-memory data store)並加入一些測試數據。 打開ContentView.swift並宣告一個靜態變數,如下所示:

@MainActor
let previewContainer: ModelContainer = {
    do {
        let container = try ModelContainer(for: ToDoItem.self,
                                           ModelConfiguration(inMemory: true))

        for index in 0..<10 {
            let newItem = ToDoItem(name: "To do item #\(index)")
            container.mainContext.insert(newItem)
        }

        return container
    } catch {
        fatalError("Failed to create container")
    }
}()

在以上的程式碼,我們建立了一個 ModelContainer 的實體,並將 inMemory 參數設置為 true。 然後,我們添加 10 個測試待辦事項並將它們保存到數據暫存器。

現在讓我們切換到 ContentView.swift 並像這樣更新預覽程式碼:

#Preview {
    ContentView()
        .modelContainer(previewContainer)
}

透過附加 modelContainer 修飾器並將其設定為預覽容器,內容視圖現在可以加載示例待辦事項並將顯示在預覽畫面中。

總結

在本章中,我們將 Todo 範例App加入永久儲存數據功能。 我希望你現在了解如何在 SwiftUI 開發中使用 SwiftData 並知道如何執行所有基本的 CRUD(建立、讀取、更新和刪除)操作。SwiftUI框架新加入的 @FetchRequest 屬性包裝器使得在數據庫管理變得非常容易。

在本章中,我們將待辦事項清單應用程式從將資料儲存在記憶體中轉變為持久性儲存。 我希望您現在更了解如何將 SwiftData 整合到 SwiftUI 專案中以及如何執行所有基本 CRUD(建立、讀取、更新和刪除)操作。 新功能如「@Query」屬性包裝器、模型容器和 Model Context 等,使得管理持久性儲存中的資料變得異常簡單和有效率。

本章所講解的範例以及最後完整的Xcode 專案:

https://www.appcoda.com/resources/swiftui5/SwiftUIToDoList.zip

請自行下載以供你參考。