You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何为ForEach内的Habit元素添加追踪数据

习惯追踪APP修改问题解决方案

问题描述

我正在学习Swift,开发一款习惯追踪APP。当前视图通过ForEach循环遍历Habit数组展示习惯,包含计数追踪和时长追踪两种类型:点击计数型习惯的+按钮时,需向其TrackingData数组添加元素以增加计数;时长型习惯则需将输入的时长添加到TrackingData数组以累计总时长。但因ForEach中的元素为常量,无法直接修改,寻求解决方法。

核心解决方案

ForEach遍历得到的habit是只读常量,无法直接修改数组内元素的属性。要实现数据更新,需通过索引定位目标习惯,或在数据管理类中封装修改方法,既保证响应式更新,也符合模块化设计思路。

步骤1:优化数据结构(消除可选值+添加初始化)

调整Habit与TrackingData的结构,避免频繁的nil判断,同时添加初始化方法简化对象创建:

import Foundation

class Habit: Codable, Identifiable, ObservableObject {
    class TrackingData: Codable, Identifiable {
        var id: Date
        var duration: Int?
        
        init(id: Date = Date.now, duration: Int? = nil) {
            self.id = id
            self.duration = duration
        }
    }
    
    var id: String
    var description: String?
    var trackingType: String
    // 将可选数组改为默认空数组,减少nil判断
    var trackingData: [TrackingData] = []
    
    init(id: String = UUID().uuidString, description: String? = nil, trackingType: String) {
        self.id = id
        self.description = description
        self.trackingType = trackingType
    }
}

步骤2:在Habits类中封装数据修改方法

在数据管理类Habits中添加专门的更新方法,统一处理数据修改与持久化,同时手动触发视图更新:

import Foundation

class Habits: ObservableObject {
    @Published var items = [Habit]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(items) {
                UserDefaults.standard.set(encoded, forKey: "Habits")
            }
        }
    }
    
    init() {
        if let savedItems = UserDefaults.standard.data(forKey: "Habits") {
            if let decodedItems = try? JSONDecoder().decode([Habit].self, from: savedItems) {
                items = decodedItems
                return
            }
        }
        items = []
    }
    
    // 计数型习惯:添加一条追踪记录
    func incrementCount(for habitId: String) {
        guard let index = items.firstIndex(where: { $0.id == habitId }) else { return }
        items[index].trackingData.append(Habit.TrackingData())
        // 手动通知视图更新(修改数组元素属性时需触发)
        objectWillChange.send()
    }
    
    // 时长型习惯:添加带时长的追踪记录
    func addDuration(for habitId: String, duration: Int) {
        guard let index = items.firstIndex(where: { $0.id == habitId }) else { return }
        items[index].trackingData.append(Habit.TrackingData(duration: duration))
        objectWillChange.send()
    }
    
    // 计算某个习惯的累计时长
    func totalDuration(for habitId: String) -> Int {
        guard let habit = items.first(where: { $0.id == habitId }) else { return 0 }
        return habit.trackingData.compactMap({ $0.duration }).reduce(0, +)
    }
}

步骤3:修改ContentView实现交互更新

调整ForEach遍历方式,使用索引获取可修改的习惯引用,并调用封装的方法完成数据更新:

import SwiftUI

struct ContentView: View {
    @StateObject var habits = Habits()
    
    @State private var showingAddHabit = false
    // 为每个时长型习惯单独存储输入值,避免共用变量导致混乱
    @State private var durationInputs: [String: Int] = [:]
    
    var body: some View {
        NavigationStack {
            ScrollView {
                if habits.items.isEmpty {
                    VStack(alignment: .leading) {
                        Text("暂无追踪习惯")
                        Text("添加一个习惯开始追踪吧")
                    }
                    .frame(maxWidth: .infinity)
                    .foregroundColor(.white.opacity(0.8))
                } else {
                    // 通过索引遍历,获取可修改的习惯引用
                    ForEach(habits.items.indices, id: \.self) { index in
                        let habit = habits.items[index]
                        VStack(alignment: .leading) {
                            HStack {
                                Text(habit.id)
                                    .padding(.leading)
                                    .frame(maxWidth: .infinity, alignment: .leading)
                                    .foregroundColor(.white.opacity(0.8))
                                
                                if habit.trackingType == "Count" {
                                    Image(systemName: "plus.circle")
                                        .frame(maxWidth: .infinity, alignment: .leading)
                                        .foregroundColor(.white.opacity(0.5))
                                    
                                    Text("\(habit.trackingData.count)")
                                        .foregroundColor(.white.opacity(0.8))
                                    
                                    Button {
                                        habits.incrementCount(for: habit.id)
                                    } label: {
                                        Image(systemName: "plus.app")
                                            .padding(.horizontal)
                                            .frame(maxWidth: .infinity, alignment: .trailing)
                                            .foregroundColor(.white.opacity(0.8))
                                    }
                                } else {
                                    Image(systemName: "clock")
                                        .frame(maxWidth: .infinity, alignment: .leading)
                                        .foregroundColor(.white.opacity(0.5))
                                    
                                    Text("\(habits.totalDuration(for: habit.id)) minutes")
                                        .foregroundColor(.white.opacity(0.8))
                                    
                                    TextField("时长", value: $durationInputs[habit.id, default: 5], format: .number)
                                        .padding(.horizontal)
                                        .multilineTextAlignment(.trailing)
                                        .keyboardType(.decimalPad)
                                        .frame(maxWidth: 80)
                                        .foregroundColor(.white.opacity(0.8))
                                        .onSubmit {
                                            let inputDuration = durationInputs[habit.id, default: 5]
                                            habits.addDuration(for: habit.id, duration: inputDuration)
                                            // 可选:提交后重置输入框
                                            durationInputs[habit.id] = 5
                                        }
                                }
                            }
                            .padding(.vertical)
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .background(Color(UIColor.systemGray5))
                        }
                        .padding(.horizontal)
                        .padding(.bottom, 8)
                    }
                }
            }
            .navigationTitle("HabiTrack")
            .background(Color(UIColor.systemGray6))
            .preferredColorScheme(.dark)
            .toolbar {
                Button {
                    showingAddHabit = true
                } label: {
                    Image(systemName: "plus.app")
                        .foregroundColor(.white)
                }
            }
            .sheet(isPresented: $showingAddHabit) {
                EditHabitsView(habits: habits)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

关键说明

  1. 索引遍历:通过ForEach(habits.items.indices)获取数组索引,进而访问可修改的习惯对象。
  2. 封装方法:数据修改逻辑集中在Habits类中,保证代码模块化,同时手动调用objectWillChange.send()触发视图刷新(修改数组元素属性时需手动通知)。
  3. 独立输入存储:使用字典durationInputs为每个习惯单独存储输入值,避免多个习惯共用变量导致的交互混乱。

内容的提问来源于stack exchange,提问作者Rob The Ranger

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 01:38:13