如何以Protocol为数据类型使用Navigation Stack?解决Hashable兼容报错
问题分析与解决方案
你遇到的Type 'any SpendCategoryModel' cannot conform to 'Hashable'错误,核心原因是Swift的存在类型(any Protocol)本身不会自动继承协议的一致性——虽然你的SpendCategoryModel协议继承了Hashable,但any SpendCategoryModel作为类型擦除后的抽象类型,编译器无法保证所有实现类的Hashable行为统一,因此拒绝将其视为符合Hashable。
直接用泛型协议(存在类型)作为导航数据源是可行的,但需要避开类型擦除带来的限制,以下是几种可行的解决方案:
方案1:用枚举封装所有协议实现
将不同的SpendCategoryModel实现类包装到一个枚举中,利用枚举天然的Hashable特性(只要关联类型符合Hashable):
// 定义枚举封装所有协议实现 enum SpendCategory: Hashable, Codable { case food(FoodCategory) case transport(TransportCategory) // 暴露协议要求的属性和方法 var name: String { switch self { case .food(let cat): return cat.name case .transport(let cat): return cat.name } } func getIds() -> [Int] { switch self { case .food(let cat): return cat.getIds() case .transport(let cat): return cat.getIds() } } } // 修改ViewModel的数组类型 class CategoryLookupViewModel: ObservableObject { @Published var filteredCategories: [SpendCategory] = [] // 其他业务逻辑 } // 更新View代码 struct CategoryLookupView: View { @ObservedObject var categoryLookupViewModel: CategoryLookupViewModel var body: some View { NavigationStack { List { Section { ForEach(categoryLookupViewModel.filteredCategories, id: \.name) { category in NavigationLink(value: category) { Text(category.name) } } } } .navigationDestination(for: SpendCategory.self) { category in // 根据枚举类型跳转对应页面或统一处理 switch category { case .food(let cat): CardResultView(spendCategory: cat) case .transport(let cat): CardResultView(spendCategory: cat) } } } } }
方案2:为存在类型手动实现Hashable
通过扩展any SpendCategoryModel,手动实现==和hash(into:)方法,强制让存在类型符合Hashable:
extension any SpendCategoryModel: Hashable { static func == (lhs: any SpendCategoryModel, rhs: any SpendCategoryModel) -> Bool { // 确保类型相同,且核心属性一致 type(of: lhs) == type(of: rhs) && lhs.name == rhs.name && lhs.getIds() == rhs.getIds() } func hash(into hasher: inout Hasher) { // 混合类型标识、name和id列表生成哈希值 hasher.combine(ObjectIdentifier(type(of: self))) hasher.combine(name) hasher.combine(getIds()) } }
注意:这种方式存在风险——如果不同实现类有相同的name和getIds()结果,但实际是不同类型,会被误判为相等。仅在你能保证核心属性足够区分不同实例时使用。
方案3:使用泛型View
将CategoryLookupView改为泛型,约束为SpendCategoryModel,让View针对具体类型工作:
// 泛型View struct CategoryLookupView<Category: SpendCategoryModel>: View { @ObservedObject var viewModel: CategoryLookupViewModel<Category> var body: some View { NavigationStack { List { Section { ForEach(viewModel.filteredCategories, id: \.name) { category in NavigationLink(value: category) { Text(category.name) } } } } .navigationDestination(for: Category.self) { category in CardResultView(spendCategory: category) } } } } // 泛型ViewModel class CategoryLookupViewModel<Category: SpendCategoryModel>: ObservableObject { @Published var filteredCategories: [Category] = [] // 其他业务逻辑 } // 使用时传入具体类型 let viewModel = CategoryLookupViewModel<FoodCategory>() let view = CategoryLookupView(viewModel: viewModel)
这种方式最符合Swift类型系统的设计,避免了类型擦除的问题,同时保持了代码的通用性。
内容的提问来源于stack exchange,提问作者yambo
相关产品推荐
相关产品推荐

