iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0

昨天我們設計了分類資料的模型,並初始化了一些預設資料。今天,我們將專注於如何使用這些資料來建立分類列表頁面,讓使用者可以查看並管理他們的分類。

目標

今天的目標是實作一個頁面,使用者可以在這個頁面上瀏覽所有分類,進行刪除操作,並提供一個入口,讓使用者在下一步可以新增分類。這裡 UI 的樣式參考了簡單記帳

  • 實作 CategoryListViewModel 來管理分類資料的邏輯。
  • 使用 CategoryListView 將分類資料呈現於頁面中。
  • 實作刪除分類的功能,並動態更新畫面。

https://ooorito.com/wp-content/uploads/2024/09/%E7%B5%90%E6%9E%9C.gif

ViewModel 設計

實作 CategoryListViewModel

我們先來建立管理分類的 ViewModel – CategoryListViewModel 吧!

這個 ViewModel 將負責處理分類管理頁面的顯示與刪除邏輯,包括從資料庫中載入分類資料、顯示分類列表,以及刪除分類的功能。我們會依賴 DataManager 來與 Core Data 進行互動。

我們需要在 ViewModel 中實作以下功能:

  • 顯示子分類資料的陣列:categories,用來儲存從資料庫中抓取的所有子分類。
  • 顯示失敗訊息:failHandle,用來處理錯誤訊息,當刪除失敗時,顯示錯誤提示。
  • 負責控制 Core Data 資料的物件:DataManager,這是我們與 Core Data 互動的主要接口。
  • 抓取子分類的函數fetchCategories(),用來從資料庫中載入所有已存在的子分類。
  • 刪除分類的函數deleteCategory( category: ItemCategory),負責刪除指定的分類,並在刪除成功後重新抓取資料。如果刪除失敗,則會顯示錯誤訊息。
    以下是 CategoryListViewModel 的程式碼:
import SwiftUI

class CategoryListViewModel: ObservableObject {
    @Published var categories: [ItemCategory] = []
    @Published var failHandle: (isFail: Bool, title: String) = (isFail: false, title: "")

    private let dataManager: DataManager
        
    init(dataManager: DataManager = DataManager()) {
        self.dataManager = dataManager
        fetchCategories()
    }
    
    func fetchCategories() {
        categories = dataManager.fetchItemCategories()
    }
    
    func deleteCategory(_ category: ItemCategory) {
        let result = dataManager.deleteItemCategory(category)
        if !result {
            failHandle = (isFail: true, title: "發生錯誤")
        } else {
            fetchCategories()
        }
    }
}

接下來,我們將實作畫面,並將 ViewModel 連結到頁面中。

View 設計

實作 CategoryView

在 CategoryListView 中,每個分類資料會顯示為一個方塊,其中包含 icon 與分類名稱。我們首先來實作 CategoryView,用於顯示單個分類的內容。

1. 定義基本結構與屬性

我們要讓 CategoryView 接收分類的 icon 和名稱作為輸入,因此我們在 struct 中定義了兩個屬性:iconName 和 name。

struct CategoryView: View {
    let iconName: String
    let name: String
  • iconName:用來儲存每個分類對應的 SF Symbols icon名稱。
  • name:分類的名稱,顯示在 icon 下方。
    這些屬性是由外部傳入的,當顯示每個分類時,會動態傳入不同的 icon 和名稱。

2. 建立 icon 與名稱的 UI 呈現

在 body 中,我們使用了 VStack 來垂直排列 icon 和名稱。這裡我們選擇使用 SF Symbols,並使用 Text 顯示分類名稱。

var body: some View {
    VStack {
        Image(systemName: iconName)   // 使用 SF Symbols  icon 
            .resizable()
            .scaledToFit()
            .frame(width: 40, height: 40)
            .foregroundColor(Color(.black))   // 設定 icon 顏色
            
        Text(name)
            .lineLimit(1)   // 限制為一行,避免名稱過長
            .truncationMode(.tail)  // 當名稱超過一行時,顯示...
            .font(.caption)   // 設定文字大小
    }
  • Image(systemName:):使用 SF Symbols,iconName 是傳入的 icon 名稱。resizable() 使 icon 可以調整大小,scaledToFit() 讓圖片保持比例不變,並限制大小為 40x40。
  • Text(name):顯示分類的名稱。使用 lineLimit(1) 保持顯示一行,若名稱過長會用 truncationMode(.tail) 顯示省略符號。

3. 設置框架與背景

接著,我們對整個 VStack 設定框架大小和背景樣式,讓它看起來像一個完整的卡片。

.frame(width: UIScreen.main.bounds.width / 4 - 20, height: UIScreen.main.bounds.width / 4 - 20)
.background(Color.white)   // 設定背景為白色
.cornerRadius(10)   // 設置圓角
.shadow(radius: 5)   // 設定陰影
  • frame:設定畫面的寬度和高度,這裡我們根據螢幕寬度自動計算出分類方塊的大小,讓每行能均勻排列四個分類方塊。
  • background:設定背景顏色為白色,使每個分類都能以白色背景展示。
  • cornerRadius:將方塊的四角設為圓角。
  • shadow:設定陰影效果,使方塊有輕微的立體感。
    最後放上完整程式碼:
import SwiftUI

struct CategoryView: View {
    let iconName: String
    let name: String

    var body: some View {
        VStack {
            Image(systemName: iconName)
                .resizable()
                .scaledToFit()
                .frame(width: 40, height: 40)
                .foregroundColor(Color(.black))
            
            Text(name)
                .lineLimit(1)
                .truncationMode(.tail)
                .font(.caption)
        }
        .frame(width: UIScreen.main.bounds.width / 4 - 20, height: UIScreen.main.bounds.width / 4 - 20)
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 5)
    }
}

#Preview {
    CategoryView(iconName: "fork.knife", name: "廚房用品")
}

實作 CategoryListView

CategoryListView 負責顯示目前已建立的分類,並允許使用者進行刪除操作。

1. 初始化與狀態管理

在 CategoryListView 中,我們透過 State 變數來管理編輯狀態 (isEditing),並且使用 -@ObservedObject 監聽 CategoryListViewModel 以獲取分類資料。

import SwiftUI
import AlertToast

struct CategoryListView: View {
    @State private var isEditing = false
    @ObservedObject private var viewModel: CategoryListViewModel
    
    init(viewModel: CategoryListViewModel = CategoryListViewModel()) {
        self.viewModel = viewModel
    }
}

2. 分類顯示與刪除功能

我們使用 LazyVGrid 來呈現分類,並在分類上方提供刪除按鈕(僅在編輯模式時顯示)。這個設計讓使用者可以方便地瀏覽分類並進行刪除操作。

let columns = [
    GridItem(.flexible(), spacing: 16),
    GridItem(.flexible(), spacing: 16),
    GridItem(.flexible(), spacing: 16),
    GridItem(.flexible(), spacing: 16)
]
var body: some View {
    ZStack {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(viewModel.categories, id: \.id) { category in
                    ZStack(alignment: .topTrailing) {
                        CategoryView(iconName: category.iconName, name: category.name)
                        
                        if isEditing {
                            Button(action: {
                                viewModel.deleteCategory(category)
                            }) {
                                Image(systemName: "minus.circle.fill")
                                    .foregroundColor(.red)
                                    .offset(x: 10, y: -10)
                            }
                        }
                    }
                }
            }
            .padding()
            .onAppear {
                viewModel.fetchCategories()
            }
        }
    }       
}

3. 切換編輯模式

透過 Toolbar,我們讓使用者可以自由切換編輯模式,並在編輯模式下提供新增分類的入口。

.toolbar {
    Button(action: {
        isEditing.toggle()
    }) {
        Image(systemName: isEditing ? "checkmark" : "square.and.pencil")
            .font(.title2)
            .foregroundColor(.blue)
    }
}

4. 新增分類

當進入編輯模式時,頁面底部會顯示「新增分類」的按鈕,點擊後導覽至 AddCategoryView。

VStack {
    Spacer()
    if isEditing {
        NavigationLink(destination: AddCategoryView()) {
            Text("新增分類")
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
                .padding(.horizontal)
        }
        .padding(.bottom, 20)
    }
}

完整程式碼:

import SwiftUI
import AlertToast

struct CategoryListView: View {
    @State private var isEditing = false
    @ObservedObject private var viewModel: CategoryListViewModel
    
    init(viewModel: CategoryListViewModel = CategoryListViewModel()) {
        self.viewModel = viewModel
    }
    
    let columns = [
        GridItem(.flexible(), spacing: 16),
        GridItem(.flexible(), spacing: 16),
        GridItem(.flexible(), spacing: 16),
        GridItem(.flexible(), spacing: 16)
    ]
    
    var body: some View {
        ZStack {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 16) {
                    ForEach(viewModel.categories, id: \.id) { category in
                        ZStack(alignment: .topTrailing) {
                            CategoryView(iconName: category.iconName, name: category.name)
                            
                            if isEditing {
                                Button(action: {
                                    viewModel.deleteCategory(category)
                                }) {
                                    Image(systemName: "minus.circle.fill")
                                        .foregroundColor(.red)
                                        .offset(x: 10, y: -10)
                                }
                            }
                        }
                    }
                }
                .padding()
                .onAppear {
                    viewModel.fetchCategories()
                }
            }
            
            VStack {
                Spacer()
                if isEditing {
                    NavigationLink(destination: AddCategoryView()) {
                        Text("新增分類")
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                            .padding(.horizontal)
                    }
                    .padding(.bottom, 20)
                }
            }
        }
        .navigationTitle("分類列表")
        .toolbar {
            Button(action: {
                isEditing.toggle()
            }) {
                Image(systemName: isEditing ? "checkmark" : "square.and.pencil")
                    .font(.title2)
                    .foregroundColor(.blue)
            }
        }
        .navigationBarTitleDisplayMode(.inline)
        .toast(isPresenting: $viewModel.failHandle.isFail, alert: {
            AlertToast(type: .error(Color.red), title: viewModel.failHandle.title)
        })
        
    }
}


#Preview {
    CategoryListView()
}

這樣就完成啦!

https://ooorito.com/wp-content/uploads/2024/09/%E7%B5%90%E6%9E%9C.gif

總結

今天我們成功完成分類列表頁面的實作,讓使用者可以瀏覽、刪除分類,並進入編輯模式管理分類。這樣的設計不僅提升了使用者的互動性,還讓分類管理變得更加直覺。在下一篇文章中,我們將繼續實作新增分類的功能,敬請期待!


上一篇
Day 15: SwiftUI 資料設計與初始化
下一篇
Day 17: SwiftUI 新增分類功能實作
系列文
用 SwiftUI 掌控家庭日用品庫存30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言