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

SwiftUI中ViewModel无法更新视图的问题求助

SwiftUI条件视图不更新:修改ViewModel的@Published变量后视图未切换

我在SwiftUI中使用条件视图时遇到问题:修改ViewModel的@Published变量后,视图没有自动切换,必须重新显示才会更新。不确定是否和SwiftData有关,尝试将dailyMacros改为视图中的@State属性包装器,但问题依旧。

视图代码

struct DiaryView: View {
    @Environment(\.modelContext) private var modelContext
    @Query var macros: [Macros]
    @ObservedObject var viewModel = DiaryViewModel()
    
    private func initView() {
        viewModel.fetchMacros(macros, viewModel.currentDate)
        if viewModel.dailyMacros == nil {
            modelContext.insert(Macros())
            viewModel.fetchMacros(macros, viewModel.currentDate)
        }
    }
    
    var body: some View {
        VStack {
            DailyDateView(date: $viewModel.currentDate)
                .onChange(of: viewModel.currentDate) { _ , newValue in
                    viewModel.fetchMacros(macros, newValue)
                }
            if viewModel.dailyMacros == nil {
                VStack(alignment: .center) {
                    Spacer()
                    Text("No data")
                        .bold()
                        .font(.title)
                    Spacer()
                }
            } else {
                HStack {
                    ProgressCircleView(number1: viewModel.dailyMacros!.carbs, number2: viewModel.totalCarbs, color: Color.brown, size: 80, title: NSLocalizedString("Carbs", comment: ""))
                    ProgressCircleView(number1: viewModel.dailyMacros!.fat, number2: viewModel.totalFat, color: Color.orange, size: 80, title: NSLocalizedString("Fat", comment: ""))
                    ProgressCircleView(number1: viewModel.dailyMacros!.proteins, number2: viewModel.totalProteins, color: Color.red, size: 80, title: NSLocalizedString("Proteins", comment: ""))
                }
                
                HStack {
                    ProgressView()
                        .progressViewStyle(CustomProgressBar(number1: viewModel.dailyMacros!.calories, number2: viewModel.totalCalories, color: Color.purple, width: 300, title: "Calories"))
                }
                .padding()
                
                HStack {
                    IncrementButton(number: $viewModel.incrementCarbs, width: 20, color: Color.brown)
                    IncrementButton(number: $viewModel.incrementFat, width: 20, color: Color.orange)
                    IncrementButton(number: $viewModel.incrementProteins, width: 20, color: Color.red)
                }
                
                HStack {
                    CustomButtonView(action: {
                        viewModel.add()
                    }, label: "Appliquer", color: Color.blue, width: 200, height: 50)
                }
            }
            Spacer()
        }
        .onAppear {
            initView()
        }
    }
}

ViewModel代码

class DiaryViewModel: ObservableObject {
    
    private enum Constants {
        static let quickAddMaxNumber: CGFloat = 999
        static let quickAddMinimalNumber: CGFloat = 0
    }
    
    @Published var incrementCarbs: CGFloat
    @Published var incrementProteins: CGFloat
    @Published var incrementFat: CGFloat
    @Published var dailyMacros: Macros?
        
    var totalCarbs: CGFloat
    var totalFat: CGFloat
    var totalProteins: CGFloat
    var totalCalories: CGFloat
    
    @Published var currentDate: Date
    
    init(incrementCarbs: CGFloat = 0, incrementProteins: CGFloat = 0, incrementFat: CGFloat = 0, dailyMacros: Macros? = nil, currentDate: Date = .now) {
        self.incrementCarbs = incrementCarbs
        self.incrementProteins = incrementProteins
        self.incrementFat = incrementFat
        self.dailyMacros = dailyMacros
        
        self.totalCarbs = (UserDefaults.standard.string(forKey: "carbs")?.toCGFloat())!
        self.totalFat = (UserDefaults.standard.string(forKey: "fat")?.toCGFloat())!
        self.totalProteins = (UserDefaults.standard.string(forKey: "proteins")?.toCGFloat())!
        self.totalCalories = (UserDefaults.standard.string(forKey: "calories")?.toCGFloat())!
        
        self.currentDate = currentDate
    }
    
    func add() {
        guard let macros = dailyMacros else {
            return
        }
        
        if checkQuickEntrys() {
            
            macros.carbs += incrementCarbs
            macros.fat += incrementFat
            macros.proteins += incrementProteins
            
            let caloriesFromCarbs = incrementCarbs * 4
            let caloriesFromFat = incrementFat * 9
            let caloriesFromProteins = incrementProteins * 4
            
            macros.calories += caloriesFromCarbs + caloriesFromFat + caloriesFromProteins
            
        }
        resetMacroIncrements()
    }

    private func resetMacroIncrements() {
        incrementCarbs = 0
        incrementFat = 0
        incrementProteins = 0
    }
    
    private func checkQuickEntrys() -> Bool {
        guard let macros = dailyMacros,
              macros.carbs + incrementCarbs >= Constants.quickAddMinimalNumber,
                  macros.fat + incrementFat >= Constants.quickAddMinimalNumber,
                  macros.proteins + incrementProteins >= Constants.quickAddMinimalNumber else {
                return false
            }
        
        guard let macros = dailyMacros,
                  macros.carbs < Constants.quickAddMaxNumber,
                  macros.fat < Constants.quickAddMaxNumber,
                  macros.proteins < Constants.quickAddMaxNumber else {
                return false
            }
        
        return true
    }

    func fetchMacros(_ macros: [Macros], _ date: Date) {
        self.dailyMacros = macros.first(where: { Calendar.current.isDate($0.date, equalTo: date, toGranularity: .day) }) ?? nil
    }
}

问题分析与解决方法

1. 替换@ObservedObject为@StateObject

在视图中直接初始化@ObservedObject会导致视图重建时重新创建ViewModel实例,无法正确保持状态监听。SwiftUI中持有ObservableObject应该用@StateObject,它会在视图生命周期内维持唯一实例:

// 替换原有的@ObservedObject
@StateObject var viewModel = DiaryViewModel()

2. 修复新插入Macros的日期缺失问题

插入新Macros时没有设置date属性,导致fetchMacros无法找到对应日期的数据,这是视图一直显示"No data"的核心原因之一:

private func initView() {
    viewModel.fetchMacros(macros, viewModel.currentDate)
    if viewModel.dailyMacros == nil {
        let newMacros = Macros()
        newMacros.date = viewModel.currentDate // 新增:设置当前日期
        modelContext.insert(newMacros)
        try? modelContext.save() // 立即保存到SwiftData
    }
}

3. 监听@Query数据更新,自动刷新dailyMacros

@Query的macros数组会在SwiftData数据变化时自动更新,添加监听来同步ViewModel的dailyMacros:

var body: some View {
    VStack {
        // ...原有视图代码
    }
    .onAppear {
        initView()
    }
    .onChange(of: macros) { newValue in // 新增:监听macros变化
        viewModel.fetchMacros(newValue, viewModel.currentDate)
    }
}

4. 手动触发ViewModel更新(修改Macros内部属性时)

当修改dailyMacros指向的Macros对象内部属性时,由于对象引用未变,@Published不会自动发送更新通知。需要在修改前手动触发ViewModel的更新:

func add() {
    guard let macros = dailyMacros else {
        return
    }
    
    if checkQuickEntrys() {
        objectWillChange.send() // 新增:手动触发视图更新
        macros.carbs += incrementCarbs
        macros.fat += incrementFat
        macros.proteins += incrementProteins
        
        let caloriesFromCarbs = incrementCarbs * 4
        let caloriesFromFat = incrementFat * 9
        let caloriesFromProteins = incrementProteins * 4
        
        macros.calories += caloriesFromCarbs + caloriesFromFat + caloriesFromProteins
        try? modelContext.save() // 新增:保存修改到SwiftData
    }
    resetMacroIncrements()
}

5. 让ViewModel持有ModelContext(可选优化)

为了在ViewModel中直接操作SwiftData,可以在初始化时传入ModelContext:

// ViewModel修改
class DiaryViewModel: ObservableObject {
    private let modelContext: ModelContext // 新增
    
    // 修改init方法
    init(modelContext: ModelContext, incrementCarbs: CGFloat = 0, incrementProteins: CGFloat = 0, incrementFat: CGFloat = 0, dailyMacros: Macros? = nil, currentDate: Date = .now) {
        self.modelContext = modelContext
        // ...原有初始化代码
    }
    
    // add方法中直接使用modelContext
    func add() {
        guard let macros = dailyMacros else {
            return
        }
        
        if checkQuickEntrys() {
            objectWillChange.send()
            macros.carbs += incrementCarbs
            macros.fat += incrementFat
            macros.proteins += incrementProteins
            
            let caloriesFromCarbs = incrementCarbs * 4
            let caloriesFromFat = incrementFat * 9
            let caloriesFromProteins = incrementProteins * 4
            
            macros.calories += caloriesFromCarbs + caloriesFromFat + caloriesFromProteins
            try? modelContext.save()
        }
        resetMacroIncrements()
    }
}

// 视图中初始化ViewModel
struct DiaryView: View {
    @Environment(\.modelContext) private var modelContext
    @Query var macros: [Macros]
    @StateObject var viewModel: DiaryViewModel // 修改为非直接初始化
    
    // 新增init方法
    init(modelContext: ModelContext) {
        self.modelContext = modelContext
        _macros = Query()
        _viewModel = StateObject(wrappedValue: DiaryViewModel(modelContext: modelContext))
    }
    
    // ...原有视图代码
}

内容的提问来源于stack exchange,提问作者Killianoni

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 13:22:02