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

SwiftUI中如何通过按钮修改单例ViewModel的数据?

问题:MVVM数据流混淆导致的SwiftUI状态更新错误

我完全混淆了MVVM数据流的概念:我有一个简单的示例视图,展示由单例ViewModel提供的Doc数组,该ViewModel需要支持多位置访问。每个Doc项包含名称和一个按钮,点击按钮需修改其isFav值,并通过按钮颜色变化反映该修改。我尝试过通过ViewModel方法、结构体mutating方法、直接修改三种方式,但每种方式都出现不同错误,请问我哪里出错了?

import SwiftUI

struct Doc: Identifiable
{
    let id: UUID
    var name: String
    var isFav: Bool = false
    
    init(_ name: String)
    {
        self.id = UUID()
        self.name = name
    }
    
    init()
    {
        self.id = UUID()
        self.name = id.uuidString.dropLast(13).lowercased()
    }
    
    mutating func updateIsFav()
    {
        self.isFav.toggle()
    }
}

class ViewModel: ObservableObject
{
    static var shared = ViewModel()
    
    private init() {}
    @Published var array: [Doc] = []

    func updateIsFav(for doc: Doc)
    {
        if let index = array.firstIndex(where: { $0.id == doc.id} )
        {
            self.array[index].isFav.toggle()
        }
    }
}

struct ContentView: View
{
    @ObservedObject var model = ViewModel.shared
    var body: some View
    {
        ZStack
        {
            Color.black.ignoresSafeArea()
            ScrollView
            {
                VStack
                {
                    ForEach( model.array, id: \.id )
                    {
                        doc in
                        HStack
                        {
                            Text( doc.name )
                                .foregroundColor(.white)
                            
                            Button(action:
                                    {
                                        doc.updateIsFav()           // Cannot use mutating member on immutable value: 'doc' is a 'let' constant
                                        doc.isFav.toggle            // Cannot reference 'mutating' method as function value
                                        model.updateisFav(for: doc) // Cannot call value of non-function type 'Binding<Subject>'
                                    },
                                   label:
                                    {
                                            Image(systemName: "star")
                                        .foregroundColor(doc.isFav ? .yellow : .white)
                                    }
                            )
                           

                        }
                    }
                }
            }
        }
        .onAppear()
        {
            model.array.append(Doc())
            model.array.append(Doc())
            model.array.append(Doc())
            model.array.append(Doc())
            model.array.append(Doc())
            model.array.append(Doc())
            model.array.append(Doc())
        }
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View 
    {
        ContentView()
            .previewDevice("iPhone 13")
    }
}

错误原因分析

  • doc.updateIsFav() 报错:ForEach循环中取出的doc是不可变的let常量,结构体的mutating方法需要修改自身实例,因此无法直接调用。
  • doc.isFav.toggle 报错:一是语法错误,toggle()是方法必须加括号;二是doc为let常量,无法直接修改其属性。
  • model.updateisFav(for: doc) 报错:ViewModel中的方法名是updateIsFav(大写I),这里写成了updateisFav(小写i),属于拼写错误,导致找不到对应方法。

修复方案与完整代码

按照MVVM设计原则,视图只负责展示和触发操作,所有状态变更都应该通过ViewModel处理,保证数据流一致性。以下是修复后的代码:

import SwiftUI

struct Doc: Identifiable
{
    let id: UUID
    var name: String
    var isFav: Bool = false
    
    init(_ name: String)
    {
        self.id = UUID()
        self.name = name
    }
    
    init()
    {
        self.id = UUID()
        self.name = id.uuidString.dropLast(13).lowercased()
    }
}

class ViewModel: ObservableObject
{
    static let shared = ViewModel() // 用let保证单例不可被意外替换
    
    private init() {}
    @Published var array: [Doc] = []

    func toggleFavorite(for docId: UUID)
    {
        if let index = array.firstIndex(where: { $0.id == docId } )
        {
            array[index].isFav.toggle()
            // @Published会自动发送通知,触发视图更新
        }
    }
}

struct ContentView: View
{
    @ObservedObject var model = ViewModel.shared
    var body: some View
    {
        ZStack
        {
            Color.black.ignoresSafeArea()
            ScrollView
            {
                VStack(spacing: 16)
                {
                    ForEach(model.array) { doc in // Doc是Identifiable,可省略id参数
                        HStack
                        {
                            Text(doc.name)
                                .foregroundColor(.white)
                            
                            Spacer() // 让按钮靠右对齐,优化布局
                            
                            Button(action: {
                                // 传递doc的ID给ViewModel修改状态,而非整个实例
                                model.toggleFavorite(for: doc.id)
                            }, label: {
                                Image(systemName: doc.isFav ? "star.fill" : "star") // 用填充星星更直观区分状态
                                    .foregroundColor(doc.isFav ? .yellow : .white)
                            })
                        }
                        .padding(.horizontal)
                    }
                }
            }
        }
        .onAppear {
            // 简化数组填充逻辑
            (0..<7).forEach { _ in
                model.array.append(Doc())
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View 
    {
        ContentView()
            .previewDevice("iPhone 13")
    }
}

核心优化点

  1. 修正拼写错误:将ViewModel方法调用修正为正确的方法名,或改用更语义化的toggleFavorite。
  2. 通过ID修改状态:传递doc的UUID而非整个实例给ViewModel,避免结构体拷贝问题,符合MVVM数据流向。
  3. 移除冗余mutating方法:状态变更统一由ViewModel处理,保持数据模型的纯粹性。
  4. 优化视图体验:添加Spacer调整布局,用填充星星图标更直观展示收藏状态。
  5. 单例用let声明:避免单例实例被意外替换,保证数据一致性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:35:06