至目前為止,我們的商品資料都是放在變數中,也就是記憶體中。不管我們新增了多少商品,關掉 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
- 在
Product
Entity 中,新增 3 個 Attribute。分別為:id
:商品的唯一識別碼,Type 為UUID
name
:商品名稱,Type 為String
price
:商品價格,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 做複雜的操作時,我們還是需要手動寫程式。但是有很多的時候,我們只是單純地讀取資料。這兩種方式都值得了解一下。