建立第一個 SwiftUI App:Day 7 – Core Data

Photo by Fabienne Hübener on Unsplash
Photo by Fabienne Hübener on Unsplash
至目前為止,我們的商品資料都是放在變數中,也就是記憶體中。不管我們新增了多少商品,關掉 App 再打開後,這些新增的商品都會不見。在 iOS 中,資料的儲存大部分會選擇 Core Data。本章將介紹如何將商品資料儲存在 Core Data。

至目前為止,我們的商品資料都是放在變數中,也就是記憶體中。不管我們新增了多少商品,關掉 App 再打開後,這些新增的商品都會不見。在 iOS 中,資料的儲存大部分會選擇 Core Data。本章將介紹如何將商品資料儲存在 Core Data。

Coffee Shop 的完整程式碼可以在 下載。

建立 Product Entity

還記得我們在建立 Xcode 專案中,建立專案時,有勾選 Use Core Data。這樣 Xcode 在建立專案時,就會幫我們產生 CoffeeShop.xcdatamodeld 檔案。這個檔案就是 Core Data 的 Schema。

我們會在這個 Schema 裡面,新增 Product 的 Schema。依照下圖的指示:

  1. 點選 CoffeeShop.xcdatamodeld
  2. 點擊 Add Entity 來新增一個 Entity
  3. 將 Entity 命名為 Product
  4. Product Entity 中,新增 3 個 Attribute。分別為:
    1. id:商品的唯一識別碼,Type 為 UUID
    2. name:商品名稱,Type 為 String
    3. price:商品價格,Type 為 Double
  5. 在右邊的 Core Data Inspector 中,將 Codegen 選為 Manual/None。
建立 Product Core Data
建立 Product Core Data

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

Product Core Data 程式
Product Core Data 程式

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。新增的商品依然還在!

Coffee Shop with Core Data
Coffee Shop with Core Data

結語

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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Alex Alvarez on Unsplash
Read More

Dispatch Queue 教學

GCD 提供有效率的並行處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。
Read More
Photo by Florinel Gorgan on Unsplash
Read More

如何製作一個 XCFramework

XCFramework 讓你可以將 iPhone、iPhone 模擬器等多的不同平台的二進位碼打包到一個可發佈的 .xcframework 檔。你只需要為你的 Framework 產生出一個 .xcframework 檔,就可以支援多種平台。
Read More
Photo by Fabian Gieske on Unsplash
Read More

SwiftUI @State & @Binding 教學

SwiftUI 推出了兩個 Property Wrapper – @State and @Binding。利用它們可以達到變數的 Two-way Binding 功能。也就是當變數的值改變時,它會重新被顯示。本章藉由製作一個 Custom View 來展示如何使用 @State 和 @Binding。
Read More
Photo by Svitlana on Unsplash
Read More

iOS:禁止螢幕截圖

基於一些理由,我們可能會想要禁止使用者對我們的 app 做螢幕截圖。然而,iOS 並沒有提供這樣的功能。所幸,我們可以利用 UITextField 來達到此效果。
Read More