在 SwiftUI 開發過程中,資料模型的設計與初始化是一個非常重要的環節,因為它決定了 App 如何儲存、處理和展示資料。
還記得我們昨天在側邊欄中新增了兩個項目:「管理分類」與「管理地點」嗎?為了讓使用者能夠清楚地對家用品進行分類,並標示它們的放置地點,我們需要新增這兩個管理功能。因此,今天的目標是為我們的家用庫存管理 App 設計並擴增分類與地點的資料模型,並透過 Core Data 進行資料的初始化,為後續的功能打下基礎。
今天的目標是設計 Core Data 中的資料模型,並實現初始化資料的邏輯,讓 App 在啟動時能自動建立預設的分類與地點資料,方便使用者在後續操作中進行物品的管理。
我們會著重以下幾個部分:
我們需要儲存兩種類型的資料:物品分類和家中地點。
為了達到這個目的,我們需要在 Core Data 中設計以下幾個資料模型:
CategoryGroup 模型的主要作用是將子分類進行分組,並為後續的顏色標示及圖表等視覺化功能打好基礎。
CategoryGroup 實體包含以下幾個屬性:
ItemCategory 模型將負責儲存每個具體分類的資訊。這個模型能幫助我們更好的管理使用者所建立的不同分類,並且能與大分類(CategoryGroup)進行關聯。
地點模型只需儲存家中的具體地點名稱和顏色,例如「客廳」、「廚房」。
Location:代表家中的具體位置,包含以下屬性:
在我們完成 CategoryGroup、ItemCategory 和 Location 模型的建立後,接下來需要手動管理這些類別。
在 Core Data 模型中,選擇你剛剛建立的實體 (CategoryGroup、ItemCategory 和 Location),然後在右側的 Class 選項中,將 Codegen 的 Name 設定為 Manual/None。這個操作是為了告訴 Xcode,我們不需要它自動生成 NSManagedObject 類別,因為我們希望手動控制這些類別的生成。
接著,打開 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 {
}
在開始初始化大分類、子分類和地點資料之前,我們先實作對資料的增、刪、改、查功能。這將幫助我們在後續的程式碼中保持一致性和可重用性。
首先,我們要讓使用者能夠新增大分類、子分類和地點資料。
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 中,我們撰寫了兩個方法來進行分類資料的初始化:
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)
}
}
}
地點資料的初始化邏輯與分類資料相似,使用相同的查詢和新增邏輯來檢查並初始化預設地點。
為了讓使用者在第一次啟動 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 時就能更方便使用。這樣的設計有效地避免了多次重複檢查資料的問題。今天就先做到這邊,明天我們將進行分類管理的頁面實作,明天見!