至目前為止,我們的商品資料都是放在變數中,也就是記憶體中。不管我們新增了多少商品,關掉 App 再打開後,這些新增的商品都會不見。在 iOS 中,資料的儲存大部分會選擇 Core Data。本章將介紹如何將商品資料儲存在 Core Data。
SwiftUI Coffee Shop
建立 Product Entity
還記得我們在建立 Xcode 專案中,建立專案時,有勾選 Use Core Data。這樣 Xcode 在建立專案時,就會幫我們產生 CoffeeShop.xcdatamodeld 檔案。這個檔案就是 Core Data 的 Schema。
我們會在這個 Schema 裡面,新增 Product 的 Schema。依照下圖的指示:
- 點選 CoffeeShop.xcdatamodeld
- 點擊 Add Entity 來新增一個 Entity
- 將 Entity 命名為
Product - 在
ProductEntity 中,新增 3 個 Attribute。分別為:id:商品的唯一識別碼,Type 為UUIDname:商品名稱,Type 為Stringprice:商品價格,Type 為Double
- 在右邊的 Core Data Inspector 中,將 Codegen 選為 Manual/None。

接下來我們要宣告 struct Product。Xcode 可以根據 Product Entity 來幫我們產生 struct Product。

Xcode 會產生 2 個檔案,Product+CoreDataClass 和 Product+CoreDataProperties。我們可以看到在 Product+CoreDataProperties 中,它有宣告 Product Entity 的 3 個 Attribute。
extension Product {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Product> {
return NSFetchRequest<Product>(entityName: "Product")
}
@NSManaged public var id: UUID?
@NSManaged public var name: String?
@NSManaged public var price: Double
}在剛剛的 Codegen 欄位中,如果我們不去修改它,讓它是 Class Definition 的話,那我們也就不需要做上一步的動作了。因為當 Codegen 是 Class Definition 時,每次 Xcode 在編譯程式碼的時候,會自動產生上面那 2 個檔案,並且一起編譯它們。這樣的話,你在專案中是看不到 struct Product 的定義宣告,但是依然可以使用它。但,我是比較偏好可以在專案中看到所有的程式碼。
修改 ProductListView
接下來,我們會用 Product 來取代 Item。我們回到 ProductListView,將它修改成從 Core Data 中讀取商品列表。下面是修改後的 ProductListView。
struct ProductListView: View {
@Environment(\.managedObjectContext) var context
@FetchRequest(entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: false)])
var products: FetchedResults<Product>
var body: some View {
List {
ForEach(products, id: \.id) { product in
ProductRowView(product: product)
}
}
.navigationBarTitle("Product List")
.navigationBarItems(trailing: NavigationLink(destination: AddProductView()) {
Image(systemName: "plus")
})
}
}NSManagedObjectContext
context 被宣告為 @Environment,之前我們在新增商品2 – Alert, Navigation有談到 @Environment。它的型態是 NSManagedObjectContext。所有對 Core Data 的存取都要透過 NSManagedObjectContext。
此外,這個 context 是在 SceneDelegate.swift 裡,被設定到 Environment 的。
// SceneDelegate.swift
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
....
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
....
}@FetchRequest
原本讀取 Core Data,是要先取得一個 NSFetchRequest,然後再呼叫 NSManagedObjectContext.fetch() 才能取得商品資料。
import CoreData
let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
do {
let products: [Product] = try context.fetch(fetchRequest)
} catch {
print(error)
}現在 @FetchRequest 這個 Property Wrapper 幫我們省下了不少程式碼。我們只要宣告好它,商品資料就會自動被讀取好。是不是方便很多呢?
而 NSSortDescriptor 是告訴 NSFetchRequest,讀取商品列表時,要對商品名稱 name 做排序。
@FetchRequest(entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: false)])
var products: FetchedResults<Product>修改 ProductRowView
在 ProductRowView 中,我們只需要將 product 的型態改為 Product 即可。另外還要再修改一下 ProductRowView_previews。
struct ProductRowView: View {
let product: Product
...
}
struct ProductRowView_Previews: PreviewProvider {
private static var product: Product {
let product = Product()
product.id = UUID()
product.name = "Americano"
product.price = 25
return product
}
static var previews: some View {
ProductRowView(product: product)
}
}修改 AddProductView
原本的 AddProductView 會將新增的商品傳回 ProductListView,然後儲存在變數中。現在可以儲存在 Core Data,所以 AddProductView 會將新增的商品儲存在 Core Data,然後直接返回 ProductListView。
struct AddProductView: View {
@Environment(\.managedObjectContext) var context
...
private func addProduct() -> Bool {
guard !name.isEmpty else {
return false
}
guard let parsedPrice = Double(price) else {
return false
}
let product = Product(context: context)
product.id = UUID()
product.name = name
product.price = parsedPrice
do {
try context.save()
return true
} catch {
print("\(error)")
return false
}
}
}
刪除 Item.swift
我們已經將所有用到 Item 的地方都改用 Product 了。Item 也就完成它階段性的任務,請直接刪除 Item.swift 吧。
執行 App
最後,讓我們來執行 App。試著新增一個商品,然後關掉 App,再開啟 App。新增的商品依然還在!

結語
SwiftUI 為 Core Data 推出新的 Property Wrapper – @FetchRequest,以及將 NSManagedObjectContext 放到 @Environment 中。這為我們省下不少的程式碼。當然如果我們要對 Core Data 做複雜的操作時,我們還是需要手動寫程式。但是有很多的時候,我們只是單純地讀取資料。這兩種方式都值得了解一下。









