You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

嵌套ForEach视图中自定义Toggle样式无法显示勾选标记的问题

嵌套ForEach视图中自定义Toggle样式无法显示勾选标记的问题

看起来你遇到的问题核心是数据模型的可观察性没有正确配置,加上自定义ToggleStyle的冗余处理,导致点击Toggle后虽然后台数据已经变化,但视图没收到刷新通知,所以勾选标记不显示。我来一步步帮你修复:

问题根源分析

  1. 嵌套对象的变化无法被追踪:你的ListItemCategorizedItem是普通类,它们的属性(比如isChecked)变化时,不会通知上层的ViewModel和SwiftUI视图。尽管ViewModel被标记为@Observable,但它只能追踪自己的直接属性(比如activeList数组的添加/删除),无法感知数组内部对象的属性变化。
  2. 自定义ToggleStyle的冗余点击处理:你在CheckboxToggleStyle里手动加了onTapGesture来切换configuration.isOn,但Toggle本身已经通过Binding处理了状态变更,这会导致逻辑重复,甚至可能引发状态不同步。
  3. Binding的使用方式不够高效:用自定义的get/set Binding来调用fetch和toggle方法没问题,但因为模型变化无法通知视图,所以视图不会刷新显示新的状态。

解决方案步骤

1. 让数据模型支持可观察性

ListItemCategorizedItem改成@Observable类,同时遵循Identifiable协议,这样它们的属性变化会被SwiftUI的Observation系统追踪,一旦isChecked改变,视图会自动刷新:

@Observable class ListItem: Identifiable {
    let id = UUID()
    var description: String
    var isChecked: Bool
    
    init(description: String, isChecked: Bool) {
        self.description = description
        self.isChecked = isChecked
    }
}

@Observable class CategorizedItem: Identifiable {
    let id = UUID()
    var category: String
    var items: [ListItem]
    
    init(category: String, items: [ListItem]) {
        self.category = category
        self.items = items
    }
}

2. 优化ViewModel的方法(适配服务器交互需求)

保留你和服务器交互的逻辑,在ViewModel里写一个专门的方法处理Toggle事件,后续可以在这个方法里加网络请求:

@Observable class ViewModel {
    var activeList: [CategorizedItem] = []
    
    init() {
        // 硬编码测试数据保持不变
        let Dairy1 = ListItem(description: "Milk", isChecked: false)
        let Dairy2 = ListItem(description: "Yogurt", isChecked: false)
        let Dairy3 = ListItem(description: "Cheese", isChecked: false)
        let DairyList = [Dairy1, Dairy2, Dairy3]
        let cat1 = CategorizedItem(category: "Dairy", items: DairyList)
        activeList.append(cat1)

        let groc1 = ListItem(description: "Onion", isChecked: false)
        let groc2 = ListItem(description: "Carrot", isChecked: false)
        let groc3 = ListItem(description: "Tomato", isChecked: false)
        let grocList: [ListItem] = [groc1, groc2, groc3]
        let cat2 = CategorizedItem(category: "Groceries", items: grocList)
        activeList.append(cat2)

        let bak1 = ListItem(description: "Bread", isChecked: false)
        let bak2 = ListItem(description: "Bagels", isChecked: false)
        let bak3 = ListItem(description: "English Muffins", isChecked: false)
        let bakList: [ListItem] = [bak1, bak2, bak3]
        let cat3 = CategorizedItem(category: "Bakery", items: bakList)
        activeList.append(cat3)
    }
    
    // 处理Toggle事件,这里可以加服务器请求逻辑
    func toggleItem(_ item: ListItem) {
        item.isChecked.toggle()
        print("TOGGLING item: \(item.description) to \(item.isChecked)")
        // 这里添加你的服务器请求代码,比如:
        // sendToggleRequest(to: item.id, isChecked: item.isChecked)
    }
}

3. 修复自定义ToggleStyle

去掉冗余的onTapGesture,让Toggle本身通过Binding处理状态变更,自定义样式只负责根据configuration.isOn显示对应的图标:

struct CheckboxToggleStyle: ToggleStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack {
            RoundedRectangle(cornerRadius: 5.0)
                .stroke(lineWidth: 1)
                .frame(width: 40, height: 40)
                .overlay {
                    Image(systemName: configuration.isOn ? "checkmark" : "square.dotted")
                        .font(.system(size: 40))
                }
        }
        .contentShape(Rectangle()) // 确保点击整个区域都能触发Toggle
    }
}

4. 优化ContentView的ForEach和Binding

现在因为数据模型都是可观察的,我们可以直接遍历数组对象,不用索引,Binding直接关联到item.isChecked,同时通过Bindingset触发ViewModel的服务器交互逻辑:

struct ContentView: View {
    @State var vm: ViewModel = ViewModel()
    
    var body: some View {
        List {
            // 外层ForEach直接遍历CategorizedItem数组
            ForEach($vm.activeList) { $categoryItem in
                VStack(alignment: .leading) {
                    Text(categoryItem.category)
                        .font(.title)
                    
                    // 内层ForEach遍历当前分类的ListItem数组
                    ForEach($categoryItem.items) { $item in
                        HStack {
                            Toggle("", isOn: Binding(
                                get: { item.isChecked },
                                set: { _ in
                                    vm.toggleItem(item)
                                }
                            ))
                            .toggleStyle(CheckboxToggleStyle())
                            
                            Text(item.description)
                        }
                    }
                }
            }
        }
    }
}

完整修改后的代码

把所有修改整合到一起的完整代码如下:

import SwiftUI
import Foundation
import Combine

@Observable class ListItem: Identifiable {
    let id = UUID()
    var description: String
    var isChecked: Bool
    
    init(description: String, isChecked: Bool) {
        self.description = description
        self.isChecked = isChecked
    }
}

@Observable class CategorizedItem: Identifiable {
    let id = UUID()
    var category: String
    var items: [ListItem]
    
    init(category: String, items: [ListItem]) {
        self.category = category
        self.items = items
    }
}

@Observable class ViewModel {
    var activeList: [CategorizedItem] = []
    
    init() {
        let Dairy1 = ListItem(description: "Milk", isChecked: false)
        let Dairy2 = ListItem(description: "Yogurt", isChecked: false)
        let Dairy3 = ListItem(description: "Cheese", isChecked: false)
        let DairyList = [Dairy1, Dairy2, Dairy3]
        let cat1 = CategorizedItem(category: "Dairy", items: DairyList)
        activeList.append(cat1)

        let groc1 = ListItem(description: "Onion", isChecked: false)
        let groc2 = ListItem(description: "Carrot", isChecked: false)
        let groc3 = ListItem(description: "Tomato", isChecked: false)
        let grocList: [ListItem] = [groc1, groc2, groc3]
        let cat2 = CategorizedItem(category: "Groceries", items: grocList)
        activeList.append(cat2)

        let bak1 = ListItem(description: "Bread", isChecked: false)
        let bak2 = ListItem(description: "Bagels", isChecked: false)
        let bak3 = ListItem(description: "English Muffins", isChecked: false)
        let bakList: [ListItem] = [bak1, bak2, bak3]
        let cat3 = CategorizedItem(category: "Bakery", items: bakList)
        activeList.append(cat3)
    }
    
    func toggleItem(_ item: ListItem) {
        item.isChecked.toggle()
        print("TOGGLING item: \(item.description) to \(item.isChecked)")
        // 在这里添加你的服务器请求逻辑
    }
}

struct CheckboxToggleStyle: ToggleStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack {
            RoundedRectangle(cornerRadius: 5.0)
                .stroke(lineWidth: 1)
                .frame(width: 40, height: 40)
                .overlay {
                    Image(systemName: configuration.isOn ? "checkmark" : "square.dotted")
                        .font(.system(size: 40))
                }
        }
        .contentShape(Rectangle())
    }
}

struct ContentView: View {
    @State var vm: ViewModel = ViewModel()
    
    var body: some View {
        List {
            ForEach($vm.activeList) { $categoryItem in
                VStack(alignment: .leading) {
                    Text(categoryItem.category)
                        .font(.title)
                    
                    ForEach($categoryItem.items) { $item in
                        HStack {
                            Toggle("", isOn: Binding(
                                get: { item.isChecked },
                                set: { _ in
                                    vm.toggleItem(item)
                                }
                            ))
                            .toggleStyle(CheckboxToggleStyle())
                            
                            Text(item.description)
                        }
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

@main
struct StupidMeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

验证效果

现在你点击Checkbox的时候,toggleItem方法会被调用,item.isChecked会切换,因为ListItem是@Observable,这个变化会立即通知SwiftUI视图,自定义ToggleStyle会根据新的configuration.isOn显示对应的勾选图标,同时你可以在toggleItem里添加服务器请求逻辑,完全满足你的需求。

火山引擎 最新活动