SwiftUI芯片选择功能在父视图层级失效问题求助
SwiftUI芯片选择组件选中状态同步问题解决方案
问题根源分析
- 模型状态管理错误:
ChipModel结构体中使用@State修饰isSelected是错误用法,@State仅用于View内部临时状态,不应放在数据模型中。 - 值传递导致状态隔离:
ChipItemView接收的isSelected是值类型传递,修改的是本地副本,无法同步回ChipsViewModel中的原始数据,导致UI不更新选中状态。 - 双重状态维护冲突:
customDrink和ChipsViewModel同时维护选中状态,两者数据未联动,导致取消操作无法同步更新两边数据。
分步解决方案
1. 修正ChipModel数据结构
移除@State修饰符,将isSelected改为普通布尔属性,模型状态由ObservableObject统一管理:
struct ChipModel: Identifiable { var isSelected: Bool let id = UUID() let systemImage: String let imageType: String let titleCategory: String let titleKey: LocalizedStringKey }
2. 重构ChipsViewModel的状态更新逻辑
添加方法修改数组中ChipModel的选中状态(结构体是值类型,需替换整个元素触发@Published更新):
class ChipsViewModel: ObservableObject { @Published var chipArray: [ChipModel] = [ ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Basil"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Rosemary"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Tarragon"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Sage"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Thyme"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Mint"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Herbs", titleKey: "Hibiscus"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Spices", titleKey: "Cinnamon Stick"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Spices", titleKey: "Star Anise"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Spices", titleKey: "Fresh Cloves"), ChipModel(isSelected: false, systemImage: "basil", imageType: "Custom", titleCategory: "Fresh Spices", titleKey: "Rainbow Peppercorns") ] func toggleSelection(for chip: ChipModel) { guard let index = chipArray.firstIndex(where: { $0.id == chip.id }) else { return } chipArray[index].isSelected.toggle() } }
3. 修改ChipItemView使用绑定同步状态
将isSelected改为绑定关联ViewModel数据,确保修改同步到全局状态:
struct ChipItemView: View { @EnvironmentObject var customdrink: customDrink @EnvironmentObject var viewModel: ChipsViewModel @State private var showingAlert = false let chip: ChipModel var body: some View { VStack { HStack(spacing: 4) { Text(chip.titleKey) .foregroundColor(.red) } .padding(.vertical, 5) .padding(.horizontal, 10) .background(chip.isSelected ? Color.blue : Color.white) .cornerRadius(20) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(Color.black, lineWidth: 1.5) ) .onTapGesture { handleTap() } } .alert("选择数量过多,请先取消一个选项", isPresented: $showingAlert) { Button("确定", role: .cancel) {} } } private func handleTap() { guard let garnishKey = chip.titleKey.stringKey else { return } if customdrink.myDrinkGarnishCount < 5 { viewModel.toggleSelection(for: chip) if chip.isSelected { customdrink.myDrinkGarnishCount += 1 customdrink.myDrinkGarnishAr.append(garnishKey) } else { customdrink.myDrinkGarnishCount -= 1 if let index = customdrink.myDrinkGarnishAr.firstIndex(of: garnishKey) { customdrink.myDrinkGarnishAr.remove(at: index) } } } else if customdrink.myDrinkGarnishCount == 5 && chip.isSelected { viewModel.toggleSelection(for: chip) customdrink.myDrinkGarnishCount -= 1 if let index = customdrink.myDrinkGarnishAr.firstIndex(of: garnishKey) { customdrink.myDrinkGarnishAr.remove(at: index) } } else { showingAlert.toggle() } } } struct ChipItemView_Previews: PreviewProvider { static var previews: some View { let viewModel = ChipsViewModel() ChipItemView(chip: viewModel.chipArray[0]) .environmentObject(customDrink()) .environmentObject(viewModel) .previewLayout(.sizeThatFits) .padding() } }
4. 统一视图层级的ViewModel实例
确保整个视图树使用同一个ChipsViewModel,避免重复创建导致状态隔离:
struct ChipContainerView: View { @ObservedObject var viewModel: ChipsViewModel @EnvironmentObject var customdrink: customDrink @State var scrollViewOffset: CGFloat = 0 @State var startOffset: CGFloat = 0 @State private var pulsateButton: Bool = false let chipbaseButtonCategories = [ ChipBaseCategories(category: "Fresh Herbs"), ChipBaseCategories(category: "Fresh Spices") ] var columns: [GridItem] = [ GridItem(.adaptive(minimum: 130), spacing: 0) ] var body: some View { var width = CGFloat.zero var height = CGFloat.zero VStack { GeometryReader { geo in ZStack { Color.white.ignoresSafeArea() ScrollViewReader { proxyReader in ScrollView(.vertical, showsIndicators: false) { Spacer().id("SCROLL_TO_TOP") VStack { Text("Choose Up to 5") .padding(.top, 25) ZStack(alignment: .topLeading) { LazyVGrid( columns: columns, alignment: .leading, pinnedViews: [.sectionHeaders] ) { ForEach(chipbaseButtonCategories) { chipbaseCategory in Section(header: Text(chipbaseCategory.category).font(.system(size: 15,weight: .bold)).padding(10)) { let catFilter = viewModel.chipArray.filter { $0.titleCategory == chipbaseCategory.category} ForEach(catFilter) { data in ChipItemView(chip: data) .environmentObject(viewModel) .padding(.all, 3) .padding(.horizontal, 3) .alignmentGuide(.leading) { dimension in if (abs(width - dimension.width) > geo.size.width) { width = 0 height -= dimension.height } let result = width if data.id == viewModel.chipArray.last!.id { width = 0 } else { width -= dimension.width } return result } .alignmentGuide(.top) { dimension in let result = height if data.id == viewModel.chipArray.last!.id { height = 0 } return result } } } } } } Spacer() Text("Current Item Count \(customdrink.myDrinkGarnishCount)") .padding(.top, 25) .padding(.bottom, 25) } .padding(.horizontal, 5) .padding(.bottom, 25) .overlay( GeometryReader { proxy -> Color in DispatchQueue.main.async { if startOffset == 0 { self.startOffset = proxy.frame(in: .global).minY } let offset = proxy.frame(in: .global).minY self.scrollViewOffset = offset - startOffset } return Color.clear } .frame(width: 0, height: 0 ) ,alignment: .top ) } .overlay( Button(action: { proxyReader.scrollTo("SCROLL_TO_TOP", anchor: .top) }, label: { Text("BACK TO TOP") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) .padding() .background(Color.blue) .clipShape(Capsule()) .shadow(color: Color.black.opacity(0.09), radius: 5, x: 5, y: 5) }) .padding(.trailing) .padding(.bottom, getSafeArea().bottom == 0 ? 12: 0) .frame(maxHeight: .infinity, alignment: .bottomTrailing) .opacity(-scrollViewOffset > 250 ? 1 : 0) .animation(.easeOut,value: pulsateButton) ,alignment: .bottomTrailing ) .onAppear() { pulsateButton.toggle() } } } } } } } struct Garnish1TypeView: View { @EnvironmentObject var customdrink: customDrink @StateObject var viewModel = ChipsViewModel() var body: some View { ZStack { Color.gray.ignoresSafeArea() VStack(spacing: 10) { Text("Select Your Garnish") .font(.title) .fontWeight(.bold) ChipContainerView(viewModel: viewModel) .environmentObject(viewModel) Spacer() } .padding(.horizontal, 10) } } }
5. 清理customDrink中的冗余状态
移除myDrinkGarnishSelected属性,该状态已由ChipsViewModel统一管理:
class customDrink: ObservableObject { @Published var myDrinkName: String = "" @Published var myDrinkType: String = "" @Published var myDrinkSize: String = "" @Published var myDrinkServingSize: String = "" @Published var myDrinkBase1: String = "" @Published var myDrinkBase2: String = "" @Published var myDrinkSweet: String = "" @Published var myDrinkSour: String = "" @Published var myDrinkGarnishAr: [String] = [] @Published var myDrinkGarnishCount: Int = 0 }
内容的提问来源于stack exchange,提问作者brandon aho
相关产品推荐
相关产品推荐

