iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
Mobile Development

從零開始學習 iOS系列 第 28

從零開始學習 iOS Day27 - 專案實作 飲食紀錄清單頁

  • 分享至 

  • xImage
  •  

在前一篇中,我們讓使用者能夠新增飲食紀錄,但目前資料仍是存在記憶體中,一旦重新啟動 App,資料就會消失。
今天,我們要讓使用者能夠查看飲食紀錄清單頁,並且透過 SwiftData 將資料永久保存!


今日目標

  1. 建立「飲食紀錄清單頁」顯示所有餐點紀錄。
  2. 透過 SwiftData 實現「資料持久化」,讓 App 關掉再開也能保留資料。

建立資料模型(Model)

我們先將原本的 MealRecord 改寫為 SwiftData 的資料實體。

import Foundation
import SwiftData

@Model
class MealRecord {
    var id: UUID
    var name: String
    var calories: Int
    var category: MealCategory
    var date: Date
    
    init(
        id: UUID = UUID(),
        name: String,
        calories: Int,
        category: MealCategory,
        date: Date = .now
    ) {
        self.id = id
        self.name = name
        self.calories = calories
        self.category = category
        self.date = date
    }
}

並確保餐點分類 MealCategory 能被編碼與儲存:

enum MealCategory: String, Codable, CaseIterable {
    case breakfast = "早餐"
    case lunch = "午餐"
    case dinner = "晚餐"
    case snack = "點心"
}

調整Repository

接著我們調整Repository,為了取得SwiftData我們必須把ModelContext注入進來。並且因為是IO行為所以使用async/await來處理

接著我們要改寫MealRepository,讓它能透過SwiftData操作資料。由於存取資料屬於 I/O 行為,因此使用async/await進行非同步處理。

import SwiftData
import Foundation

class MealRepository: ObservableObject {
    static let shared = MealRepository()
    
    @Published private(set) var records: [MealRecord] = []
    
    private var modelContext: ModelContext?
    
    func configure(context: ModelContext) async{
        self.modelContext = context
        await fetchAll()
    }
    
    func addMeal(name: String, calories: Int, category: MealCategory) async{
        guard let context = modelContext else { return }
                
        let newMeal = MealRecord(
            name: name,
            calories: calories,
            category: category,
            date: .now
        )
        
        context.insert(newMeal)
                
        do {
            try context.save()
            await fetchAll() // 重新抓取資料
        } catch {
            print("無法儲存資料:\(error)")
        }
    }
    
    func fetchAll() async{
        guard let context = modelContext else { return }
            
        let descriptor = FetchDescriptor<MealRecord>(
            sortBy: [SortDescriptor(\.date, order: .reverse)]
        )
            
        do {
            records = try context.fetch(descriptor)
        } catch {
            print("無法讀取資料:\(error)")
        }
    }
    
    
    func delete(_ record: MealRecord) async{
        guard let context = modelContext else { return }
        context.delete(record)
        
        do {
            try context.save()
            await fetchAll()
        } catch {
            print("無法刪除資料:\(error)")
        }
    }
}

App 入口設定

確保整個 App 都有 SwiftData 環境。

在App主程式中加上.modelContainer(for:)。

import SwiftUI

@main
struct ITHelpSideProjectApp: App {
    var body: some Scene {
        WindowGroup {
            DashboardView()
        }
        .modelContainer(for: MealRecord.self)
    }
}

建立ViewModel

ViewModel 將會負責串接 Repository 並提供給 UI 使用。

import SwiftUI
import SwiftData

class MealListViewModel: ObservableObject {
    @Published var records: [MealRecord] = []
    
    private let repository = MealRepository.shared
    
    init() {
        // 監聽 repository 的變化
        repository.$records
            .receive(on: RunLoop.main)
            .assign(to: &$records)
    }
    
    func configure(context: ModelContext) {
        Task {
            await repository.configure(context: context)
        }
    }
    
    /// 重新從資料庫載入
    func refresh() {
        Task {
            await repository.fetchAll()
        }
    }
    
    /// 新增一筆餐點紀錄
    func addMeal(name: String, calories: Int, category: MealCategory) {
        Task {
            await repository.addMeal(name: name, calories: calories, category: category)
        }
    }
    
    /// 刪除一筆紀錄
    func delete(at offsets: IndexSet) {
        for index in offsets {
            let record = records[index]
            Task {
                await repository.delete(record)
            }
        }
    }
}

建立「飲食紀錄清單頁」

import SwiftUI
import SwiftData

struct MealListView: View {
    @Environment(\.modelContext) private var modelContext
    @StateObject private var viewModel = MealListViewModel()
    
    var body: some View {
        VStack() {
            if (viewModel.records.isEmpty) {
                Text("尚未新增餐點")
                    .foregroundColor(.gray)
            } else {
                List {
                    ForEach(viewModel.records) { meal in
                        VStack(alignment: .leading, spacing: 6) {
                            Text(meal.name)
                                .font(.headline)
                            Text("\(meal.calories) 大卡")
                                .foregroundColor(.gray)
                            Text(meal.category.rawValue)
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                        .padding(.vertical, 4)
                    }
                    .onDelete {indexSet in
                        viewModel.delete(at:indexSet)
                    }
                }
            }
        }.navigationTitle("飲食紀錄")
            .toolbar {
                NavigationLink(destination: AddMealView()) {
                    Image(systemName: "plus")
                }
            }
            .onAppear {
                viewModel.configure(context: modelContext)
            }
    }
    
}

#Preview {
    MealListView()
}

從首頁導向清單頁

Section {
    NavigationLink(destination: MealListView()) {
        Label("查看所有飲食紀錄", systemImage: "list.bullet.rectangle")
            .font(.headline)
            .foregroundColor(.orange)
    }
    .frame(maxWidth: .infinity, alignment: .center)
}

預覽成果

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day27/%E8%9E%A2%E5%B9%95%E9%8C%84%E5%BD%B1%202025-10-12%20%E6%99%9A%E4%B8%8A11.03.48.gif?raw=true

完成後的清單頁能夠:

  • 顯示所有新增的餐點紀錄
  • 支援刪除紀錄

今日小結

今天我們完成了飲食記錄 App 的清單功能

  • 顯示所有飲食紀錄
  • 資料持久化(App 關閉後仍保留)
  • 支援刪除與即時更新

上一篇
從零開始學習 Jetpack Compose Day26 - 專案實作 新增飲食記錄頁
下一篇
從零開始學習 iOS Day28 - 專案實作 詳細紀錄頁
系列文
從零開始學習 iOS30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言