能否将SwiftUI ForEach的标识符作为参数传入泛型视图?
泛型复选框列表视图的标识符参数化方案
问题背景
我有一个泛型结构体,用于接收对象列表并展示复选框列表,通过泛型支持任意类型对象,同时用函数参数指定对象的显示文本。目前ForEach使用id:\.self作为标识符,只要对象符合Identifiable和Hashable协议就能正常工作。
但这是KMM项目的一部分,有时处理的对象并不符合Identifiable/Hashable协议。虽然可以用显式标识符(比如id:\.uuid)解决,但会导致大量重复代码,无法复用这个泛型视图。我希望将标识符作为参数传入结构体,默认值为\.self,比如:var forEachIdentifier = \T.self,然后在ForEach中使用ForEach(options, id: forEachIdentifier){...}。
原结构体代码:
struct SelectMultipleChoiceDialog<T:Hashable>: View { var onDismissRequest: () -> Void var title: String var options: [T] var selectedOptions: [T] var text: (T) -> String var onCheckedChange: (T, Bool) -> Void var body: some View { NavigationStack{ ScrollView{ LazyVStack { ForEach(options, id:\.self){option in HStack{ Toggle(isOn: Binding(get: { selectedOptions.contains(option) }, set: { value in onCheckedChange(option, value) })){ } .toggleStyle(iOSCheckboxToggleStyle()) Text(text(option)) } .padding(.vertical, 6) } } } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: Button("Done"){ onDismissRequest() }) } } }
解决方案
可以通过KeyPath实现标识符的参数化,同时调整泛型约束让视图更灵活:
- 调整泛型定义:移除
T: Hashable的强制约束,改为要求标识符的类型ID符合Hashable,通过泛型参数传入 - 添加标识符参数:用
KeyPath<T, ID>类型接收标识符路径,默认值设为\.self(此时要求T本身是Hashable,作为默认兼容场景) - 适配选中判断逻辑:将原
selectedOptions.contains(option)改为基于标识符的匹配,避免依赖T的Hashable实现
修改后的完整代码:
struct SelectMultipleChoiceDialog<T, ID: Hashable>: View { var onDismissRequest: () -> Void var title: String var options: [T] var selectedOptions: [T] var text: (T) -> String var onCheckedChange: (T, Bool) -> Void var id: KeyPath<T, ID> // 默认初始化:当T本身是Hashable时,自动用\.self作为标识符 init(onDismissRequest: @escaping () -> Void, title: String, options: [T], selectedOptions: [T], text: @escaping (T) -> String, onCheckedChange: @escaping (T, Bool) -> Void, id: KeyPath<T, ID> = \.self) where T == ID { self.onDismissRequest = onDismissRequest self.title = title self.options = options self.selectedOptions = selectedOptions self.text = text self.onCheckedChange = onCheckedChange self.id = id } // 自定义标识符的初始化 init(onDismissRequest: @escaping () -> Void, title: String, options: [T], selectedOptions: [T], text: @escaping (T) -> String, onCheckedChange: @escaping (T, Bool) -> Void, id: KeyPath<T, ID>) { self.onDismissRequest = onDismissRequest self.title = title self.options = options self.selectedOptions = selectedOptions self.text = text self.onCheckedChange = onCheckedChange self.id = id } var body: some View { NavigationStack{ ScrollView{ LazyVStack { ForEach(options, id: id){option in HStack{ Toggle(isOn: Binding(get: { // 基于标识符判断选中状态 selectedOptions.contains(where: { $0[keyPath: id] == option[keyPath: id] }) }, set: { value in onCheckedChange(option, value) })){ } .toggleStyle(iOSCheckboxToggleStyle()) Text(text(option)) } .padding(.vertical, 6) } } } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: Button("Done"){ onDismissRequest() }) } } }
使用示例
- 默认场景(T符合Hashable):无需传入标识符参数
SelectMultipleChoiceDialog( onDismissRequest: { /* 处理关闭逻辑 */ }, title: "选择选项", options: ["选项1", "选项2", "选项3"], selectedOptions: ["选项1"], text: { $0 }, onCheckedChange: { option, isChecked in /* 处理选中变化 */ } ) - 自定义标识符(T不符合Hashable):传入对象的唯一属性路径
// 示例KMM非Hashable数据类 struct KMMItem { let uuid: String let name: String } SelectMultipleChoiceDialog( onDismissRequest: { /* 处理关闭逻辑 */ }, title: "选择KMM项", options: [KMMItem(uuid: "1", name: "项1"), KMMItem(uuid: "2", name: "项2")], selectedOptions: [], text: { $0.name }, onCheckedChange: { item, isChecked in /* 处理选中变化 */ }, id: \.uuid )
内容的提问来源于stack exchange,提问作者Charlie Walker
相关产品推荐
相关产品推荐

