
Learn not to add too many features right away, and get the core idea built and tested.
– Leah Culver
恭喜你達到這一里程碑 !到目前為止,你已經成功開發一個基本的 App 來讓使用者列出他們最愛的餐廳。至此,所有的餐廳都已在原始碼中預先定義,並儲存在陣列中。如果你要新增餐廳,最簡單的方式是將其加到現有的 restaurants 陣列中。
但是,如果你採用此方式,新餐廳資料將不會永久儲存,儲存在記憶體(如陣列)的資料是短暫的,一旦你離開App,所有的變更都將遺失,因此我們需要決定一種持久性儲存資料的方式。
要實現永久資料儲存,我們需要將資料儲存在檔案或資料庫等持久性儲存器( Persistent Storage ),例如:透過將資料儲存到資料庫,則即使 App 閃退或當機,資料也將保持安全。另一方面,檔案更適合儲存不需要頻繁修改的少量資料,其通常用於儲存 App 的設定,如 Info.plist 檔。
FoodPin App 可能需要儲存數千筆餐廳紀錄,使用者經常會增加或刪除紀錄,在這種情況下,資料庫是管理大型資料集的合適解決方案。在本章中,我將引導你了解 SwiftData 框架,並示範如何利用它來處理資料庫操作,我們會討論使用 SwiftData 框架建立資料模型和執行 CRUD(建立、讀取、更新與刪除)等主題。
你將對現有的 FoodPin 專案進行大量的更改,但是在完成本章之後,你的 App 將可讓使用者永久儲存他們最喜愛的餐廳。
首先,必須注意的是 SwiftData 框架不應該與資料庫混淆。SwiftData 建立在 Core Data 之上,實際上是一個框架,旨在幫助開發者管理持久性儲存器上的資料,並與之互動。雖然 iOS 的預設持久性儲存器通常是SQLite 資料庫,但值得注意的是持久性儲存器也可以採用其他形式,例如:Core Data 還可以用於管理本機檔案中的資料(如XML 檔案)。
無論你使用的是 Core Data 還是 SwiftData 框架,這兩種工具都可以保護開發者避免受到底層持久儲存的複雜性的影響。以 SQLite 資料庫為例,使用 SwiftData,則無須擔心連接到資料庫或理解 SQL 才能取得資料紀錄;相反的,開發者可以專注於使用 API 和 Swift 巨集(如 @Query 和 @Model),來有效管理 App 中的資料。
iOS 17 中新導入了 SwiftData 框架,以取代之前的 Core Data 框架。自 Objective-C 時代以來,Core Data 一直是 iOS 開發的資料管理 API,儘管開發者可以將該框架整合到 Swift 專案中,但是 Core Data 並非 Swift 和 SwiftUI 的原生解決方案。
在 iOS 17 中,Apple 終於為 Swift 導入了一個名為「SwiftData」的原生框架,用於持久性資料管理和資料模型建立。它建立在 Core Data 之上,但是 API 完全重新設計,以最大化利用 Swift。

如果你之前使用過 Core Data,你可能會記得必須使用資料模型編輯器來建立一個資料模型(檔案副檔名為「.xcdatamodeld」),以實現資料持久性,如圖 18.1 所示。而隨著 SwiftData 的發布,你不再需要這麼做了,SwiftData 使用巨集簡化了整個過程,這是 iOS 17 中的另一個新 Swift 功能。例如:你已經為歌曲定義了一個模型類別,如下所示:
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
}
這就是在程式碼中定義資料模型的架構(Schema)的方式。透過這個簡單的關鍵字, SwiftData 會自動啟用資料類別的持久性,並提供其他資料管理功能,例如:iCloud 同步。屬性(Attribute )是從屬性(Property )中推斷出來的,它支援基本的值型別,例如:Int 和 String。
SwiftData 讓你使用屬性元資料(Property Metadata )自訂架構的建置方式,你可以使用 @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 中,你可以在應用程式的根(root )設定模型容器:
import SwiftData
import SwiftUI
@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer (for: [Song.self, Album.self]))
}
}
當設定模型容器後,你就可以開始使用模型內容(Model Context)來取得和儲存資料。內容用作追蹤更新、取得資料、儲存變更、甚至取消這些更改的介面。使用 SwiftUI 時, 你通常可以從視圖環境中取得模型內容:
struct ContextView: View {
@Environment(\.modelContext) private var modelContext
}
有了內容,你就可以取得資料了。最簡單的方式是使用 @Query 屬性包裹器,只需一行程式碼,即可輕鬆載入和篩選儲存於資料庫中的任何內容。
@Query(sort: \.artist, order: .reverse) var songs: [Song]
要在持久性儲存器中插入項目,你可以呼叫模型內容的 insert 方法,並向其傳送要插入的模型物件。
modelContext.insert(song)
同樣的,你可以透過模型內容刪除項目,如下所示:
modelContext.delete(song)
這是對 SwiftData 的簡要介紹,如果你仍然不確定如何使用 SwiftData,也無須擔心, 當我們將 FoodPin App 從記憶體儲存器轉換為持久性儲存器時,你將清楚了解其用法。
現在,讓我們回到 FoodPin 專案。如你所知,Restaurant 結構作為我們的模型類別。要將其與 SwiftData 整合,第一步是將其遷移並轉換為 SwiftData 的資料模型。
切換到 Restaurant.swift,並導入 SwiftData 和 SwiftUI 套件:
import SwiftData
import SwiftUI
接下來,將 Restaurant 類別替換為下列的程式碼:
@Model class Restaurant {
enum Rating: String, CaseIterable {
case awesome
case good
case okay
case bad
case terrible
var image: String {
switch self {
case .awesome: return "love"
case .good: return "cool"
case .okay: return "happy"
case .bad: return "sad"
case .terrible: return "angry"
}
}
}
var name: String = ""
var type: String = ""
var location: String = ""
var phone: String = ""
var summary: String = ""
@Attribute(.externalStorage) var imageData = Data()
@Transient var image: UIImage {
get {
UIImage(data: imageData) ?? UIImage()
}
set {
self.imageData = newValue.pngData() ?? Data()
}
}
var isFavorite: Bool = false
@Transient var rating: Rating? {
get {
guard let ratingText = ratingText else {
return nil
}
return Rating(rawValue: ratingText)
}
set {
self.ratingText = newValue?.rawValue
}
}
@Attribute(originalName: "rating") var ratingText: Rating.RawValue?
init(name: String, type: String, location: String, phone: String, description: String, image: UIImage = UIImage(), isFavorite: Bool = false, rating: Rating? = nil) {
self.name = name
self.type = type
self.location = location
self.phone = phone
self.summary = description
self.image = image
self.isFavorite = isFavorite
self.rating = rating
}
}
原來的 Restaurant 類別是 ObservableObject 的子類別,要將其轉換為 SwiftData 的模型類別,你只需使用 @Model 巨集對其進行標註,並刪除類別擴展。此外,不再需要使用 @Published 標註屬性。
我們有 rating 屬性,它的型別是 Enum。Rating 列舉保持不變,但是 rating 屬性現在已經成為負責處理評價文字轉換的計算屬性。在我們的程式碼中,我們繼續使用 Rating 列舉來處理餐廳評價。然而,由於 SwiftData 將評價儲存為文字,我們需要一種方法在 rating 和 ratingText 之間建立橋樑,因此我們建立 rating 屬性作為計算屬性。在 getter 中,我們將 ratingText 轉換回列舉;在 setter 中,我們取得列舉的原始值,並將其指派給 ratingText。
你可能知道,我們用 @Transient 標註 rating 屬性,此註釋指示 SwiftData 不要將該屬性儲存在資料庫中。SwiftData 負責儲存和管理的是 ratingText 屬性。
對於原來的影像屬性,現在將其轉換為 Data 型別的 imageData,因為:
@Attribute(.externalStorage) var imageData = Data()
在 SwiftData 中,你可以選擇利用 @Attribute(.externalStorage) 巨集來將大數據與資料庫分開儲存。此巨集指示 SwiftData 將屬性儲存在與資料庫檔案不同的單獨檔案中。
為了方便起見,我們還包含了 image 屬性,它是一個計算屬性,負責將圖片資料轉換為 UIImage 物件。
最後,我們對 description 屬性進行修改,將其變更為 summary,因為 description 是 SwiftData 中的保留字(Reserved Word )。
現在我們已經準備好模型類別,是時候修改程式碼,以從資料庫中取得紀錄了。切換到 RestaurantListView.swift,你應該會看到相當多的錯誤,但別擔心,我們將一一修復它們。首先,我們在檔案的開頭匯入 SwiftData 框架:
import SwiftData
原先我們有一個存放範例餐廳資料的陣列變數,它也使用 @State 標記:
@State var restaurants = [ Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: "cafedeadend", isFavorite: false),
.
.
.
]
由於我們要將項目儲存在資料庫中,因此我們需要修改這行程式碼,並從資料庫取得資料。在 SwiftData 中,它有一個名為「@Query」的屬性包裹器,可讓你輕鬆地從資料庫載入資料。
使用 @Query 來替換上列的程式碼,如下所示:
@Query var restaurants: [Restaurant]
這個 @Query 屬性會自動為你取得所需的資料。在上列的程式碼中,我們指定取得 Restaurant 物件,當我們取得餐廳項目後,我們利用 List 視圖來顯示項目。
image 屬性的型別現在更改為「UIImage」,我們無法再使用圖片的檔名來實例化圖片視圖,因此你應該會在各種來源檔案中看到許多與 image 屬性相關的錯誤。
在 BasicTextImageRow 視圖中,將 Image 視圖的程式碼從:
Image(restaurant.image)
改為:
Image(uiImage: restaurant.image)
同樣的,將下列的程式碼:
if let imageToShare = UIImage(named: restaurant.image) {
ActivityView(activityItems: [defaultText, imageToShare])
} else {
ActivityView(activityItems: [defaultText])
}
改為:
ActivityView(activityItems: [defaultText, restaurant.image])
圖 18.2 所示的程式碼還有另一個錯誤,這裡我們傳送 Restaurant 物件的綁定,但是我們使用 @Query 屬性包裹器更新了 restaurants 變數,這就是為何 Xcode 向你顯示錯誤的原因。

要修復這個錯誤,則將 BasicTextImageRow 的綁定從:
@Binding var restaurant: Restaurant
改為:
@Bindable var restaurant: Restaurant
iOS 17 中導入的 @Bindable 屬性包裹器可能對你來說比較陌生,它的功能與 @Binding 類似,但專門設計用於與 @Observable 物件建立關聯。當你使用 @Model 將模型類別轉換為 SwiftData 模型類別時,該類別將成為遵循 @Observable 的物件,這就是為何你無法使用 @Binding 屬性包裹器而必須改用 @Bindable 的原因。
透過更改,你可以更新 BasicTextImageRow 的初始化如下:
BasicTextImageRow(restaurant: restaurants[index])
你可能還注意到 BasicTextImageRow 的 #Preview 區塊中存在另一個錯誤,如圖 18.3 所示。由於 image 參數現在是 UIImage 型別,因此我們需要在初始化期間為其提供一個 UIImage 物件。

要解決此錯誤,你可以簡單地建立一個 UIImage 的實例,並將其傳送給 image 參數,如下所示:
#Preview("BasicTextImageRow", traits: .sizeThatFitsLayout) {
BasicTextImageRow(restaurant: Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: UIImage(named: "cafedeadend")!, isFavorite: true))
}
你應該仍會在 RestaurantListView 結構中看到一個錯誤,該錯誤與刪除餐廳有關。下列是觸發錯誤的程式碼:
restaurants.remove(atOffsets: indexSet)
要修復這個問題,則宣告一個 modelContext 變數來取得模型內容:
@Environment(\.modelContext) private var modelContext
然後,我們將在 RestaurantListView 結構中建立一個名為「deleteRecord」的新函數, 如下所示:
private func deleteRecord(indexSet: IndexSet) {
for index in indexSet {
let itemToDelete = restaurants[index]
modelContext.delete(itemToDelete)
}
}
要使用 SwiftData 來從資料庫中刪除紀錄,則你可以呼叫模型內容的 delete 方法,並向其傳送要刪除的項目。
使用新方法,你可以像這樣修改 .onDelete 修飾器:
.onDelete(perform: deleteRecord)
當使用者從清單視圖中刪除項目時,我們呼叫 deleteRecord 方法來從資料庫中永久刪除該項目。
App 還沒有準備好執行。如果你開啟 RestaurantDetailView.swift 檔,你會注意到 Xcode 顯示的一些錯誤,這些錯誤是對模型類別進行更改的結果。
首先是 Image 視圖,我們需要將 UIImage 物件傳送給它,而不是使用檔名載入圖片,因此將 Image(restaurant.image) 改為:
Image(uiImage: restaurant.image)
另一個錯誤與預覽有關。在 #Preview 區塊中,更新 RestaurantDetailView 的實例化如下,以將 UIImage 物件傳送給 image 參數:
#Preview {
NavigationStack {
RestaurantDetailView(restaurant: Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: UIImage(named: "cafedeadend")!, isFavorite: true))
}
.tint(.white)
}
由於我們已將 Restaurant 的 description 屬性替換為 Summary,因此我們還需要將下列程式碼:
Text(restaurant.description)
改為:
Text(restaurant.summary)
現在我們切換到 ReviewView.swift 來修復錯誤。將下列程式碼:
Image(restaurant.image)
改為:
Image(uiImage: restaurant.image)
對於 #Preview 區塊,更改 ReviewView 的實例化如下,以將 image 參數傳送給 UIImage 物件:
#Preview {
ReviewView(isDisplayed: .constant(true), restaurant: Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: UIImage(named: "cafedeadend")!, isFavorite: true))
}
如前所述,ModelContainer 用作模型類型的持久性後端。要 在SwiftUI 中建立 ModelContainer,則開啟 FoodPinApp.swift,並將 modelContainer 修飾器加到 WindowGroup, 如下所示:
WindowGroup {
RestaurantListView()
}
.modelContainer(for: Restaurant.self)
這為其所有視窗設定了一個共享模型容器,該容器被設定為儲存 Restaurant 的實例。現在你應該能夠使用模擬器執行 App,而不會出現錯誤。
在我們繼續討論如何使用 SwiftData 之前,我們岔開一下話題來介紹空清單視圖。此時,你應該可以在預覽窗格中看到一筆紀錄,但是當你在模擬器上執行 App 時,清單視圖顯示為空白,如果能為使用者提供一些類似圖 18.4 所示的畫面的說明,不是很好嗎?

我已經為空視圖設計了一個圖片,你可以下載本章準備的圖片包( http://www.appcoda.com/resources/swift53/emptydata.zip )。解壓縮檔案,並將圖片加入素材目錄(Assets.xcasset ),然後確認為圖片啟用「Preserve Vector Data」選項。
現在切換到 RestaurantListView.swift,將 List 視圖的程式碼從:
List {
ForEach(restaurants.indices, id: \.self) { index in
.
.
.
}
.onDelete(perform: deleteRecord)
.listRowSeparator(.hidden)
}
改為:
List {
if restaurants.count == 0 {
Image("emptydata")
.resizable()
.scaledToFit()
} else {
ForEach(restaurants.indices, id: \.self) { index in
ZStack(alignment: .leading) {
NavigationLink(destination: RestaurantDetailView(restaurant: restaurants[index])) {
EmptyView()
}
.opacity(0)
BasicTextImageRow(restaurant: restaurants[index])
}
}
.onDelete(perform: deleteRecord)
.listRowSeparator(.hidden)
}
}
我們新增一個條件來驗證 restaurants 陣列。如果它不包含任何項目,我們將顯示 emptydata 圖片。現在於任何模擬器上執行該 App,當清單為空時,你應該會看到該圖片。
現在 App 能夠從內建資料庫中取得紀錄了,我們繼續更新程式碼來看如何使用 SwiftData 儲存紀錄到資料庫中。
最佳作法是我們將建立一個視圖模型來配對 New Restaurant 表單,此視圖模型將儲存使用者輸入的值,並且我們可以選擇在此視圖模型類別中執行表單驗證。而是否必須建立此視圖模型類別呢?不,你仍然可以將所有的程式碼放在 NewRestaurantView 結構中,但是透過將程式碼與視圖結構分離,你的程式碼會變得更具可讀性且更易於管理。當你實作後,這個方法的好處就會彰顯出來。
在專案導覽器中的「FoodPin」資料夾上按右鍵,建立一個名為「ViewModel」的新群組,接下來在「ViewModel」上按右鍵,並選擇「New File...」來建立一個新檔案。 你可以選取「Swift File」模板,並將檔案命名為「RestaurantFormViewModel.swift」。
將檔案內容替換為下列的程式碼:
import SwiftUI
@Observable class RestaurantFormViewModel {
// Input
var name: String = ""
var type: String = ""
var location: String = ""
var phone: String = ""
var summary: String = ""
var image: UIImage = UIImage()
init(restaurant: Restaurant? = nil) {
if let restaurant = restaurant {
self.name = restaurant.name
self.type = restaurant.type
self.location = restaurant.location
self.phone = restaurant.phone
self.summary = restaurant.summary
self.image = restaurant.image
}
}
}
每個屬性都有其對應的表單欄位,例如:location 屬性儲存「Address」欄位的值。RestaurantFormViewModel 類別使用 @Observable 巨集進行標註,這是 iOS 17 中導入的新功能。此巨集為 RestaurantFormViewModel 加入了觀察支援,並使型態遵循 Observable 協定。透過這樣做,SwiftUI 將自動觀察屬性值的變化,並通知任何相關的更新。
現在開啟 NewRestaurantView.swift,我們將修改程式碼,以使用此視圖模型類別。首先,宣告一個變數來存放表單模型,如下所示:
@Bindable private var restaurantFormViewModel: RestaurantFormViewModel
SwiftUI 為我們提供 @Bindable 屬性包裹器,它訂閱可觀察物件,並在可觀察物件變更時更新視圖。透過使用@Bindable 標註 restaurantFormViewModel,我們可以監看其值的變化。
接下來,建立 init() 方法來實例化該視圖模型:
init() {
let viewModel = RestaurantFormViewModel()
viewModel.image = UIImage(named: "newphoto") ?? UIImage()
restaurantFormViewModel = viewModel
}
我們現在將修改程式碼,以使用視圖模型。首先要做的是刪除下列的程式碼:
@State var restaurantName = ""
@State private var restaurantImage = UIImage(named: "newphoto")!
我們不再使用狀態變數儲存所選的圖片及餐廳名稱,相反的,我們會將圖片及名稱都儲存在視圖模型中,因此將 restaurantImage 替換為 restaurantFormViewModel.image,如下所示:
Image(uiImage: restaurantFormViewModel.image)
對於. fullScreenCover 修飾器,我們需要使用 $restaurantFormViewModel.image 來更新 $restaurantImage 綁定,如下所示:
.fullScreenCover(item: $photoSource) { source in
switch source {
case .photoLibrary: ImagePicker(sourceType: .photoLibrary, selectedImage: $restaurantFormViewModel.image).ignoresSafeArea()
case .camera: ImagePicker(sourceType: .camera, selectedImage: $restaurantFormViewModel.image).ignoresSafeArea()
}
}
並更新所有的文字欄位和文字視圖,以使用視圖模型:
FormTextField(label: "NAME", placeholder: "Fill in the restaurant name", value: $restaurantFormViewModel.name)
FormTextField(label: "TYPE", placeholder: "Fill in the restaurant type", value: $restaurantFormViewModel.type)
FormTextField(label: "ADDRESS", placeholder: "Fill in the restaurant address", value: $restaurantFormViewModel.location)
FormTextField(label: "PHONE", placeholder: "Fill in the restaurant phone", value: $restaurantFormViewModel.phone)
FormTextView(label: "DESCRIPTION", value: $restaurantFormViewModel.summary, height: 100)
我們最後將表單轉換為使用視圖模型類別,現在是時候編寫程式碼了,以將餐廳資料儲存到資料庫中。要在資料庫中儲存新項目,你需要先從環境中取得模型內容:
@Environment(\.modelContext) private var modelContext
接下來,建立一個名為「save()」的新方法,如下所示:
private func save() {
let restaurant = Restaurant(name: restaurantFormViewModel.name,
type: restaurantFormViewModel.type,
location: restaurantFormViewModel.location,
phone: restaurantFormViewModel.phone,
description: restaurantFormViewModel.summary,
image: restaurantFormViewModel.image)
modelContext.insert(restaurant)
}
要將新紀錄插入資料庫,則你可以使用託管內容來建立一個 Restaurant,然後呼叫內容的 save() 函數來提交這些變更。
當使用者點擊「Save」按鈕時,將呼叫 save() 方法,因此將工具列項目的程式碼:
ToolbarItem(placement: .navigationBarTrailing) {
Text("Save")
.font(.headline)
.foregroundColor(Color("NavigationBarTitle"))
}
替換為下列的程式碼片段:
ToolbarItem(placement: .navigationBarTrailing) {
Button {
save()
dismiss()
} label: {
Text("Save")
.font(.headline)
.foregroundColor(Color("NavigationBarTitle"))
}
}
現在於模擬器上執行 App,並繼續加入新餐廳,該 App 應該成功將餐廳儲存在內建資料庫中。儲存後,App 將關閉 New Restaurant 視圖,你應該能夠在清單中看到新增的餐廳。
這就是 @Query 的強大之處,每當紀錄有更新時,它都會自動取得這些變更,並通知 SwiftUI 相應更新清單視圖。
如果我們需要更新目前餐廳的評分可怎麼辦?我們怎麼才能修改資料庫中的紀錄呢? 我們需要更改程式碼來更新紀錄嗎?
使用 SwiftData, 更新會自動發生。當你在餐廳細節視圖中對餐廳進行評分時, SwiftData 會偵測到變更,並跟著更新相應的餐廳記錄,因此無須更改任何程式碼,即可更新記錄。
現在,執行 App 並對餐廳進行評分,以測試程式碼變更。評分應永久儲存在資料庫中。
細節視圖中的心形按鈕目前無法作用,你的任務是實作其功能,並使用 SwiftData 來將相應的變更儲存到資料庫中。
我希望你現在更了解如何將 SwiftData 整合到 SwiftUI 專案中,以及如何執行所有基本 CRUD(建立、讀取、更新和刪除)操作。Apple 付出了大量的努力,讓 Swift 開發者和初學者更輕鬆進行持久性資料管理和資料模型建立。
雖然 Core Data 仍然是向下兼容的選項,但現在是時候學習 SwiftData 框架了,特別是如果你正在開發專門針對iOS 17 或更高版本的 App。採用這個新框架,來利用 SwiftData 提供的增強功能和優勢。
在本章所準備的範例檔中,有最後完整的 Xcode 專案可供你下載參考: http://www.appcoda.com/resources/swift59/swiftui-foodpin-swiftdata.zip 。
你準備好要進一步改進 App 了嗎?我期望你還跟著我的腳步,讓我們繼續看看如何新增搜尋列。