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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 16:09:55