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

SwiftUI列表选中状态不更新:点击无法取消已选中项

问题:SwiftUI单选分类按钮点击后旧选中项未取消

实现了一个单选分类按钮列表,预期点击某一分类时,该分类变为选中状态,同时取消其他所有分类的选中状态。但实际测试时,点击灰色、黄色等选项后,之前选中的项并未取消选中状态。

实际效果:
分类按钮效果

原始代码

import SwiftUI

struct Category: Identifiable {
    var name: String
    var color: Color
    var id = UUID()
    init(name: String, color: Color) {
        self.name = name
        self.color = color
    }
}

struct CategoryWrapper: Identifiable {
    var id: UUID
    let category: Category
    let isSelected: Bool
    init(category: Category, isSelected: Bool) {
        self.category = category
        self.isSelected = isSelected
        self.id = category.id
    }
}

class ViewModel: ObservableObject {
    @Published var wrappers = [CategoryWrapper]()
    var selectedIdentifier = UUID()
    private var categories: [Category] = [
        Category(name: "PURPLE", color: .purple),
        Category(name: "GRAY", color: .gray),
        Category(name: "YELLOW", color: .yellow),
        Category(name: "BROWN", color: .brown),
        Category(name: "green", color: .green),
        Category(name: "red", color: .red),
    ]
    
    init() {
        reload()
    }
    
    func reload() {
        wrappers = categories.map { CategoryWrapper(category: $0, isSelected: $0.id == selectedIdentifier) }
    }
}

typealias CategoryAction = (Category?) -> Void

struct CategoryView: View {
    var category: Category
    @State var isSelected: Bool = false
    private var action: CategoryAction?
    init(category: Category, isSelected: Bool, action: @escaping CategoryAction) {
        self.category = category
        self.isSelected = isSelected
        self.action = action
    }
    
    var body: some View {
        Button {
            isSelected.toggle()
            action?(isSelected ? category : nil)
        } label: {
            Text(category.name)
                .font(.caption)
                .foregroundColor(.white)
                .background(isSelected ? category.color : .clear)
                .frame(width: 150, height: 24)
                .cornerRadius(12)
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        ScrollView {
            Text("Categories")
            ForEach(viewModel.wrappers) { wrapper in
                CategoryView(
                    category: wrapper.category,
                    isSelected: wrapper.isSelected
                ) { category in
                    viewModel.selectedIdentifier = category?.id ?? UUID()
                    viewModel.reload()
                }
            }
        }
    }
}

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

问题原因

  1. 子View内部状态与ViewModel状态脱节:CategoryView使用@State存储isSelected状态,这个状态属于View实例自身。当ViewModel更新wrappers数组时,SwiftUI会复用已有CategoryView实例(因为Category的id未变),不会用新的isSelected值覆盖View内部的@State,导致旧的选中状态被保留。
  2. 初始化选中状态错误:ViewModel初始化时selectedIdentifier是一个全新的UUID,和所有分类的ID都不匹配,所以初始没有任何分类被选中。

修复方案

核心思路是让选中状态完全由ViewModel控制,子View不再维护独立状态,而是直接读取ViewModel的状态并响应点击事件:

  1. 移除冗余的CategoryWrapper结构,直接在ViewModel中维护选中的分类ID。
  2. 修改CategoryView,不再使用@State,而是接收分类信息和选中状态,点击时直接通知ViewModel更新选中ID。

修改后的完整代码

import SwiftUI

struct Category: Identifiable {
    var name: String
    var color: Color
    var id = UUID()
    init(name: String, color: Color) {
        self.name = name
        self.color = color
    }
}

class ViewModel: ObservableObject {
    @Published private(set) var categories: [Category] = [
        Category(name: "PURPLE", color: .purple),
        Category(name: "GRAY", color: .gray),
        Category(name: "YELLOW", color: .yellow),
        Category(name: "BROWN", color: .brown),
        Category(name: "green", color: .green),
        Category(name: "red", color: .red),
    ]
    @Published var selectedCategoryId: UUID?
    
    // 可选:初始选中第一个分类
    init() {
        selectedCategoryId = categories.first?.id
    }
    
    func selectCategory(_ category: Category) {
        selectedCategoryId = category.id
    }
}

struct CategoryView: View {
    let category: Category
    let isSelected: Bool
    let onTap: () -> Void
    
    var body: some View {
        Button(action: onTap) {
            Text(category.name)
                .font(.caption)
                .foregroundColor(.white)
                .background(isSelected ? category.color : .clear)
                .frame(width: 150, height: 24)
                .cornerRadius(12)
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        ScrollView {
            Text("Categories")
                .font(.headline)
                .padding()
            
            ForEach(viewModel.categories) { category in
                CategoryView(
                    category: category,
                    isSelected: viewModel.selectedCategoryId == category.id,
                    onTap: { viewModel.selectCategory(category) }
                )
                .padding(.vertical, 4)
            }
        }
    }
}

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

修复说明

  • ViewModel通过selectedCategoryId统一管理选中状态,所有分类的选中状态都通过selectedCategoryId == category.id计算得出,确保状态唯一且一致。
  • CategoryView不再维护内部状态,完全依赖传入的isSelected值渲染,点击时直接调用ViewModel的方法更新选中ID,彻底避免了状态不一致的问题。
  • 移除冗余结构,简化了数据模型和逻辑流程。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 16:20:23