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() } }
问题原因
- 子View内部状态与ViewModel状态脱节:
CategoryView使用@State存储isSelected状态,这个状态属于View实例自身。当ViewModel更新wrappers数组时,SwiftUI会复用已有CategoryView实例(因为Category的id未变),不会用新的isSelected值覆盖View内部的@State,导致旧的选中状态被保留。 - 初始化选中状态错误:ViewModel初始化时
selectedIdentifier是一个全新的UUID,和所有分类的ID都不匹配,所以初始没有任何分类被选中。
修复方案
核心思路是让选中状态完全由ViewModel控制,子View不再维护独立状态,而是直接读取ViewModel的状态并响应点击事件:
- 移除冗余的
CategoryWrapper结构,直接在ViewModel中维护选中的分类ID。 - 修改
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
相关产品推荐
相关产品推荐

