iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0

在 SwiftUI 開發過程中,資料模型的設計與初始化是一個非常重要的環節,因為它決定了 App 如何儲存、處理和展示資料。

還記得我們昨天在側邊欄中新增了兩個項目:「管理分類」與「管理地點」嗎?為了讓使用者能夠清楚地對家用品進行分類,並標示它們的放置地點,我們需要新增這兩個管理功能。因此,今天的目標是為我們的家用庫存管理 App 設計並擴增分類與地點的資料模型,並透過 Core Data 進行資料的初始化,為後續的功能打下基礎。

目標

今天的目標是設計 Core Data 中的資料模型,並實現初始化資料的邏輯,讓 App 在啟動時能自動建立預設的分類與地點資料,方便使用者在後續操作中進行物品的管理。

我們會著重以下幾個部分:

  • Core Data 資料模型的設計
  • 資料的初始化與儲存邏輯
  • 讓 App 在啟動時自動填充資料

資料模型設計

我們需要儲存兩種類型的資料:物品分類家中地點

  • 物品分類:每個分類會有一個名稱、一個 icon,以及一個對應的大分類(例如「食品」可能包含「零食」、「飲料」等子分類)。
  • 家中地點:每個地點代表家中的一個房間或區域(例如:客廳、廚房、臥室等),我們會儲存地點的名稱和顏色,這些資料將用於之後的管理與顯示。

為了達到這個目的,我們需要在 Core Data 中設計以下幾個資料模型:

分類模型設計

  • CategoryGroup:表示分類的大類別,例如「食品」、「家具」等。
  • ItemCategory:表示具體的分類,例如「零食」、「飲料」,並且每個 ItemCategory 都會關聯到一個 CategoryGroup。

建立 CategoryGroup 模型

CategoryGroup 模型的主要作用是將子分類進行分組,並為後續的顏色標示及圖表等視覺化功能打好基礎。

CategoryGroup 實體包含以下幾個屬性:

  • id:唯一識別碼,用來區分不同的大分類。
  • name:大分類的名稱,例如「食品」、「電器」等。
  • colorHex:用來儲存顏色,方便在畫面中標示不同分類。

https://ooorito.com/wp-content/uploads/2024/09/%E5%BB%BA%E7%AB%8BCategoryGroup-1024x282.webp

建立 ItemCategory 模型

ItemCategory 模型將負責儲存每個具體分類的資訊。這個模型能幫助我們更好的管理使用者所建立的不同分類,並且能與大分類(CategoryGroup)進行關聯。

  • id:唯一識別碼,用來區分不同的分類。
  • name:分類的名稱,例如「食品」、「清潔用品」等,方便管理和顯示。
  • iconName:分類對應的 icon 名稱,將使用 SFSymbols 庫中的圖示來表示各個分類。
  • categoryGroup:關聯 CategoryGroup,確保每個子分類對應到一個大分類。透過這個關聯,所有的子分類將繼承對應大分類的顏色,這在我們後續的 UI 設計以及圖表製作中非常重要,能夠保持視覺上的一致性。

https://ooorito.com/wp-content/uploads/2024/09/%E5%BB%BA%E7%AB%8BItemCategory-1024x485.webp

地點模型設計

建立 Location 模型

地點模型只需儲存家中的具體地點名稱和顏色,例如「客廳」、「廚房」。

Location:代表家中的具體位置,包含以下屬性:

  • id:唯一識別碼,用來區分不同地點。
  • name:地點名稱,例如「客廳」、「臥室」等。
  • colorHex:用來儲存顏色,讓每個地點可以以不同顏色顯示。

https://ooorito.com/wp-content/uploads/2024/09/%E5%BB%BA%E7%AB%8BLocation-1024x285.webp

取消自動產生類別

在我們完成 CategoryGroup、ItemCategory 和 Location 模型的建立後,接下來需要手動管理這些類別。

在 Core Data 模型中,選擇你剛剛建立的實體 (CategoryGroup、ItemCategory 和 Location),然後在右側的 Class 選項中,將 Codegen 的 Name 設定為 Manual/None。這個操作是為了告訴 Xcode,我們不需要它自動生成 NSManagedObject 類別,因為我們希望手動控制這些類別的生成。

https://ooorito.com/wp-content/uploads/2024/09/%E8%AA%BF%E6%95%B4Class%E9%81%B8%E9%A0%85.webp

接著,打開 Xcode 的主選單,依次選擇 Editor > Create NSManagedObject Subclass。Xcode 會根據你建立的實體,自動生成對應的類別檔案,這些檔案會包含 Core Data 模型的屬性和方法。這樣,我們就完成了手動生成的類別,並可以對這些類別進行自定義修改。

如果有一些不是設定為 Optional 的參數,自動產生的檔案設定錯了,我們也能夠手動更改。

import Foundation
import CoreData

@objc(CategoryGroup)
public class CategoryGroup: NSManagedObject {

}

extension CategoryGroup {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<CategoryGroup> {
        return NSFetchRequest<CategoryGroup>(entityName: "CategoryGroup")
    }

    @NSManaged public var colorHex: String
    @NSManaged public var id: UUID
    @NSManaged public var name: String

}

extension CategoryGroup : Identifiable {

}
import Foundation
import CoreData

@objc(ItemCategory)
public class ItemCategory: NSManagedObject {

}

extension ItemCategory {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<ItemCategory> {
        return NSFetchRequest<ItemCategory>(entityName: "ItemCategory")
    }

    @NSManaged public var iconName: String
    @NSManaged public var id: UUID
    @NSManaged public var name: String
    @NSManaged public var categoryGroup: CategoryGroup

}

extension ItemCategory : Identifiable {

}
import Foundation
import CoreData

@objc(Location)
public class Location: NSManagedObject {

}

extension Location {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Location> {
        return NSFetchRequest<Location>(entityName: "Location")
    }

    @NSManaged public var id: UUID
    @NSManaged public var name: String
    @NSManaged public var colorHex: String

}

extension Location : Identifiable {

}

參考資料:自動生成 model class 的 Core Data

資料的CRUD

在開始初始化大分類、子分類和地點資料之前,我們先實作對資料的增、刪、改、查功能。這將幫助我們在後續的程式碼中保持一致性和可重用性。

新增功能

首先,我們要讓使用者能夠新增大分類、子分類和地點資料。

func addCategoryGroup(name: String, color: String) -> Bool {
    let newCategory = CategoryGroup(context: container.viewContext)
    newCategory.id = UUID()
    newCategory.name = name
    newCategory.colorHex = color
    return saveContext()
}

func addItemCategory(name: String, iconName: String, categoryGroup: CategoryGroup) -> Bool {
    let newCategory = ItemCategory(context: container.viewContext)
    newCategory.id = UUID()
    newCategory.name = name
    newCategory.iconName = iconName
    newCategory.categoryGroup = categoryGroup
    return saveContext()
}

func addLocation(name: String, colorHex: String) -> Bool {
    let newLocation = Location(context: container.viewContext)
    newLocation.id = UUID()
    newLocation.name = name
    newLocation.colorHex = colorHex
    return saveContext()
}

查詢功能

查詢功能允許我們從 Core Data 中獲取大分類、子分類和地點資料,並將資料呈現於 UI 中。

func fetchCategoryGroups() -> [CategoryGroup] {
    let request: NSFetchRequest<CategoryGroup> = CategoryGroup.fetchRequest()
    do {
        return try container.viewContext.fetch(request)
    } catch {
        print("Failed to fetch categories: \(error)")
        return []
    }
}

func fetchItemCategories() -> [ItemCategory] {
    let request: NSFetchRequest<ItemCategory> = ItemCategory.fetchRequest()
    do {
        return try container.viewContext.fetch(request)
    } catch {
        print("Failed to fetch categories: \(error)")
        return []
    }
}

func fetchLocations() -> [Location] {
    let request: NSFetchRequest<Location> = Location.fetchRequest()
    do {
        return try container.viewContext.fetch(request)
    } catch {
        print("Failed to fetch locations: \(error)")
        return []
    }
}

刪除功能

刪除功能用來移除不需要的分類和地點資料。

func deleteCategoryGroup(_ categoryGroup: CategoryGroup) -> Bool {
    container.viewContext.delete(categoryGroup)
    return saveContext()
}

func deleteItemCategory(_ category: ItemCategory) -> Bool {
    container.viewContext.delete(category)
    return saveContext()
}

func deleteLocation(_ location: Location) -> Bool {
    container.viewContext.delete(location)
    return saveContext()
}

更新功能

我們同樣需要實作更新功能,讓使用者可以修改大分類、子分類和地點的資料。

func updateCategoryGroup(_ categoryGroup: CategoryGroup, name: String, colorHex: String) -> Bool {
    categoryGroup.name = name
    categoryGroup.colorHex = colorHex
    return saveContext()
}

func updateItemCategory(_ itemCategory: ItemCategory, name: String, iconName: String) -> Bool {
    itemCategory.name = name
    itemCategory.iconName = iconName
    return saveContext()
}

func updateLocation(_ location: Location, name: String, colorHex: String) -> Bool {
    location.name = name
    location.colorHex = colorHex
    return saveContext()
}

資料初始化

接下來,我們將利用已經實作的資料操作功能來初始化 App 的預設資料。這些預設資料包括:

  • 物品大分類(例如:食品、家具、文具等)
  • 物品子分類(例如:食品中的蔬果、飲品等)
  • 家中地點(例如:廚房、客廳、臥室等)

我們會在 DataManager 中實作初始化方法,並在 App 啟動時檢查是否已有這些資料,若無,則自動新增預設的大分類、子分類和地點資料。

初始化分類資料

在 DataManager 中,我們撰寫了兩個方法來進行分類資料的初始化:

  • initializeDefaultCategoryGroupsIfNeeded():負責初始化預設的大分類資料,這些大分類會有名稱和代表顏色,例如「食品」對應綠色、「電器」對應藍色。
  • initializeDefaultItemCategories():負責初始化子分類,並將每個子分類關聯到對應的大分類。子分類的資料被儲存於一個字典中,根據大分類進行迭代初始化。
func initializeDefaultCategoryGroupsIfNeeded() {
    let categoryGroups = fetchCategoryGroups()
    
    if categoryGroups.isEmpty {
        let defaultGroups = [
            ("食品", "#34C759"),
            ("電器", "#007AFF"),
            ("家具", "#763300"),
            ("消耗", "#253705"),
            ("衛生", "#900C3F"),
            ("護理", "#E6B800"),
            ("運動", "#2D3B5E"),
            ("文具", "#F04AA7"),
            ("寵物", "#AF52DE")
        ]
        
        for (name, colorHex) in defaultGroups {
            _ = addCategoryGroup(name: name, color: colorHex)
        }
    }
}

func initializeDefaultItemCategories() {
    let categories = fetchItemCategories()
    
    if categories.isEmpty {
        let categoryGroups = fetchCategoryGroups()
        
        let defaultItemCategories: [String: [(name: String, iconName: String)]] = [
            "食品": [
                ("食品", "carrot.fill"),
                ("蔬果", "leaf.fill"),
                ("飲品", "cup.and.saucer.fill"),
                ("酒", "wineglass.fill")
            ],
            "電器": [("電器", "lightbulb.fill")],
            "家具": [("家具", "bed.double.fill")],
            "消耗": [
                ("消耗品", "minus.plus.batteryblock.stack.fill"),
                ("清潔用品", "hands.and.sparkles")
            ],
            "衛生": [("衛生用品", "bandage.fill")],
            "護理": [("面膜", "face.smiling")],
            "運動": [("腳踏車", "bicycle")],
            "文具": [("文具", "pencil")]
        ]
        
        for (groupName, items) in defaultItemCategories {
            if let categoryGroup = categoryGroups.first(where: { $0.name == groupName }) {
                for item in items {
                    _ = addItemCategory(name: item.name, iconName: item.iconName, categoryGroup: categoryGroup)
                }
            }
        }
    }
}

這些方法會先檢查是否已經有大分類和子分類資料,若無,則會依據預設清單自動新增這些資料。

初始化地點資料

地點資料的初始化邏輯同樣使用新增功能來自動生成預設地點。

func initializeLocationDataIfNeeded() {
    let locations = fetchLocations()
    
    if locations.isEmpty {
        let locationData = [
            ("客廳", "#FF5733"),
            ("廚房", "#33B5FF"),
            ("臥室", "#28A745"),
            ("浴室", "#FFC107")
        ]
        
        for (name, colorHex) in locationData {
            _ = addLocation(name: name, colorHex: colorHex)
        }
    }
}

地點資料的初始化邏輯與分類資料相似,使用相同的查詢和新增邏輯來檢查並初始化預設地點。

在 HandyInventory_ironApp 中進行資料初始化

為了讓使用者在第一次啟動 App 時,能夠快速開始使用,我們需要在 App 的入口 init 方法內進行這些資料的初始化,讓 App 啟動時即完成資料的檢查與設定。

import SwiftUI

@main
struct HandyInventory_ironApp: App {
    let dataManager = DataManager()

    init() {
        // App 啟動時進行資料初始化
        dataManager.initializeDefaultCategoryGroupsIfNeeded()
        dataManager.initializeDefaultItemCategories()
        dataManager.initializeLocationDataIfNeeded()
    }
    var body: some Scene {
        WindowGroup {
            HomeView()
                .environment(\.managedObjectContext, dataManager.container.viewContext)
        }
    }
}

這樣設計的好處是,初始化只會在 App 啟動時執行一次,避免了多次重複檢查,並且將資料檢查與初始化與 App 的生命週期緊密結合。

總結

通過在 App 啟動時進行資料初始化,可以讓使用者可以在第一次使用 App 時就能更方便使用。這樣的設計有效地避免了多次重複檢查資料的問題。今天就先做到這邊,明天我們將進行分類管理的頁面實作,明天見!


上一篇
Day 14: SwiftUI 建立側邊欄
下一篇
Day 16: SwiftUI 分類列表設計與實作
系列文
用 SwiftUI 掌控家庭日用品庫存30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言