如何結合 Core Data 和 SwiftUI
本文轉載自微信公眾號「Swift社區」,作者韋弦Zhy 。轉載本文請聯系Swift社區公眾號。
core data stack
SwiftUI 和 Core Data 之間相差將近十年 —— SwiftUI 隨著 iOS 13 面世而 Core Data 則是 iPhoneOS 3 的產物;很久以前,它還沒有被稱為 iOS,因為 iPad 尚未發布。盡管時間相距遙遠,Apple 還是投入了大量工作以確保這兩種強大的技術能夠完美地相互配合使用,這意味著 Core Data 就像始終以這種方式設計一樣,已集成到 SwiftUI 中。
在此項目中,我們將僅使用少量 Core Data 的功能,但是這種功能將很快擴展——我只想首先了解一下它。當您創建 Xcode 項目時,我要求您選中 Use Core Data 框,它應該導致對項目的更改:
- 現在,您有了一個名為 Bookworm.xcdatamodeld 的文件。這描述了您的數據模型,該數據模型實際上是類及其屬性的列表。
- AppDelegate.swift 和 SceneDelegate.swift 中現在有用于設置 Core Data 的額外代碼。
設置核心數據需要兩個步驟:創建所謂的持久性容器(從容器存儲中加載并保存實際數據),然后將其注入 SwiftUI 環境中,以便我們所有的視圖都可以訪問它。
Xcode 模板已經為我們完成了這兩個步驟。
因此,剩下的就是我們要決定要在 Core Data 中存儲哪些數據,以及如何讀出這些數據。首先,我們需要打開 Bookworm.xcdatamodeld 并開始使用 Xcode 的模型編輯器描述我們的數據。
之前我們描述過這樣的數據:
- struct Student {
- var id: UUID
- var name: String
- }
但是,Core Data 不能那樣工作。您會看到,Core Data 需要提前知道我們所有數據類型的樣子,包含的內容以及它們之間的關系。這就是 “xcdatamodeld” 文件的來源:我們將類型定義為“實體”,然后在其中創建屬性作為“屬性”,Core Data 負責將其轉換為可以在運行時使用的實際數據庫布局。
為了進行試用,請點擊 “Add Entity” 按鈕創建一個新實體,然后雙擊其名稱將其重命名為 “Student”。接下來,單擊 “Attributes”表正下方的+按鈕以添加兩個屬性:“id”作為 UUID 和 “name” 作為字符串。這將告訴 Core Data 創建學生并保存他們所需的一切,因此請回到 ContentView.swift,以便我們編寫一些代碼。
使用獲取請求從 Core Data 中檢索信息——我們描述了我們想要的內容,應如何對其進行排序以及是否應使用任何過濾器,然后 Core Data 會發回所有匹配的數據。我們需要確保該獲取請求隨著時間的推移保持最新,以便在創建或刪除學生時,我們的 UI 保持同步。
SwiftUI 有一個解決方案,而且——您猜對了——這是另一個屬性包裝器。這次將其稱為@FetchRequest,它帶有兩個參數:我們要查詢的實體以及我們希望結果如何排序。它具有非常特定的格式,因此,我們首先為學生添加獲取請求——請立即將此屬性添加到 ContentView:
- @FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>
分解之后,這創建了一個獲取的“學生”實體的請求,不進行任何排序,而是將其放入名稱為students,類型為FetchedResults
從那里開始,我們可以像常規的 Swift 數組一樣開始使用學生,但是您會發現有一個陷阱。首先,一些將數組放入List的代碼:
- var body: some View {
- VStack {
- List {
- ForEach(students, id: \.id) { student in
- Text(student.name ?? "Unknown")
- }
- }
- }
- }
你發現異常了嗎?是的,student.name是可選的——它可能有一個值,也可能沒有。這是 Core Data 的一個領域,該領域會讓您大為惱火:它具有可選數據的概念,但與 Swift 的可選數據完全不同。如果我們對 Core Data 說“這不是必須的”(您可以在模型編輯器中完成),它仍然會生成可選的 Swift 屬性,因為所有 Core Data 關心的是屬性在保存時具有值——在其他時間它們可以為 nil。
您可以根據需要運行代碼,但沒有太多意義——該列表將為空,因為我們尚未添加任何數據,因此我們的數據庫為空。為了解決這個問題,我們將在列表下方創建一個按鈕,每次點擊都會添加一個新的隨機學生,但是首先我們需要一個新屬性來存儲托管對象上下文。
讓我重申一下,因為這很重要。當我們定義 “Student” 實體時,實際上發生的是 Core Data 為我們創建了一個類,該類繼承自其自身的一個類:NSManagedObject。我們無法在代碼中看到該類,因為它是在構建項目時自動生成的,就像 Core ML 的模型一樣。這些對象之所以稱為托管對象,是因為 Core Data 會照料它們:它從持久性容器中加載它們并將它們的更改也寫回。
我們所有的托管對象都位于托管對象上下文中,該上下文負責實際獲取托管對象以及保存更改等。如果需要的話,您可以有許多托管對象上下文,但這距離現在還有一段路要走——實際上,您可以長期使用它。
我們不需要創建此托管對象上下文,因為 Xcode 已經為我們創建了一個。更好的是,它已經將其添加到 SwiftUI 環境中,這就是@FetchRequest屬性包裝器起作用的原因——它使用了環境中可用的任何托管對象上下文。
因此,現在將此屬性添加到ContentView:
- @Environment(\.managedObjectContext) var moc
設置好之后,下一步是添加一個按鈕,該按鈕生成隨機的學生并將其保存在托管對象上下文中。為了幫助學生脫穎而出,我們將通過創建firstNames和lastNames數組來分配隨機名稱,然后使用randomElement()從中選擇一個。
首先在List下方添加此按鈕:
- Button("Add") {
- let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
- let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]
- let chosenFirstName = firstNames.randomElement()!
- let chosenLastName = lastNames.randomElement()!
- // more code to come
- }
**注意:**不可避免地有人會抱怨我強行對randomElement()調用,但是實際上我們只是手工創建了具有值的數組——它將永遠成功。如果您非常討厭強制拆包,則可以將其替換為空合計算和默認值。
現在,有趣的部分是:我們將使用為我們生成的 Core Data 類創建一個 Student對象。這需要附加到托管對象上下文中,以便對象知道應將其存儲在何處。然后,我們可以像通常為結構體那樣分配值。
因此,現在將這三行添加到按鈕的操作閉包中:
- let student = Student(context: self.moc)
- student.id = UUID()
- student.name = "\(chosenFirstName) \(chosenLastName)"
最后,我們需要詢問托管對象上下文以保存自身。這是一個引發函數的調用,因為理論上它可能會失敗。實際上,我們所做的一切都沒有失敗的可能,因此我們可以使用try?來調用它——–我們不在乎捕獲錯誤。
因此,請將最后一行添加到按鈕的操作中:
- try? self.moc.save()
最后,您現在應該可以運行該應用程序并對其進行嘗試——單擊幾次 “Add” 按鈕以生成一些隨機的學生,您應該看到他們滑入我們列表的某個位置。更好的是,如果您重新啟動該應用程序,您會發現學生還在,因為 Core Data 已保存了他們。
現在,您可能認為這需要大量的學習,但并不會帶來很多結果,但是您現在知道什么是實體和屬性,知道什么是托管對象和請求,并且已經了解了如何保存更改。在此項目的后面以及將來,我們都將更多地關注 Core Data,但到目前為止,您已經走了很遠。