Swift技术问询:协议类型值无法遵循Identifiable协议的解决方法
嘿,这个报错我太熟悉了!咱们先搞清楚问题出在哪,再一步步把它解决掉。
为什么会报错?
你碰到的Value of protocol type 'MenuProtocol' cannot conform to 'Identifiable'; only struct/enum/class types can conform to protocols错误,核心原因是Swift的协议本身不能直接遵循另一个协议——虽然你的SubMenu1、SubMenu2这些枚举都同时遵循了OptionProtocol和Identifiable,但OptionProtocol这个协议本身并没有声明它继承Identifiable。当你把[OptionProtocol]传给要求元素是Identifiable的TheContentView时,编译器只看到了协议类型,没办法确认所有遵循OptionProtocol的类型都满足Identifiable的要求,所以就抛出了这个错误。
解决步骤
我们只需要对协议做一点小修改,让所有遵循OptionProtocol的类型都自动满足Identifiable的要求:
让
OptionProtocol继承Identifiable
把OptionProtocol的定义改成这样,强制所有实现它的类型必须遵循Identifiable:protocol OptionProtocol: Identifiable { var name: String { get } }因为你的枚举已经实现了
id属性(用rawValue),所以不需要修改任何枚举的代码,它们自动满足这个新约束。(可选但推荐)指定
ID的具体类型
因为你的所有菜单枚举都是用Int类型的rawValue作为id,可以给OptionProtocol加上where约束,明确ID的类型,避免后续出现类型不一致的问题:protocol OptionProtocol: Identifiable where ID == Int { var name: String { get } }调整
TheContentView的约束(可选)
原来的TheContentView约束是Data.Element: Identifiable, Data.Element: Hashable,现在因为OptionProtocol已经继承了Identifiable,你可以把约束简化为Data.Element: OptionProtocol,让代码更贴合你的业务场景:struct TheContentView<Data: RandomAccessCollection, ElementView: View>: View where Data.Element: OptionProtocol { var data: Data var itemView: (Data.Element) -> ElementView var body: some View { ForEach(data) { item in itemView(item) .padding() .background(Color.purple) } } }
修改后的完整代码
protocol MenuProtocol { var submenu: [OptionProtocol] { get } } // 修改后的OptionProtocol,继承Identifiable并指定ID类型 protocol OptionProtocol: Identifiable where ID == Int { var name: String { get } } enum MainMenu: Int, Identifiable, CaseIterable, MenuProtocol, OptionProtocol { case One, Two, Three, Four, Five var id: Int { rawValue } var name: String { switch self { case .One: return "One" case .Two: return "Two" case .Three: return "Three" case .Four: return "Four" case .Five: return "Five" } } var submenu: [OptionProtocol] { switch self { case .One: return SubMenu1.allCases case .Two: return SubMenu2.allCases default: return [] } } } enum SubMenu1: Int, Identifiable, CaseIterable, OptionProtocol { case SubMenu1_Option1, SubMenu1_Option2, SubMenu1_Option3, SubMenu1_Option4, SubMenu1_Option5 var id: Int { rawValue } var name: String { switch self { case .SubMenu1_Option1: return "Submenu1 Option 1" case .SubMenu1_Option2: return "Submenu1 Option 2" case .SubMenu1_Option3: return "Submenu1 Option 3" case .SubMenu1_Option4: return "Submenu1 Option 4" case .SubMenu1_Option5: return "Submenu1 Option 5" } } } enum SubMenu2: Int, Identifiable, CaseIterable, OptionProtocol { case SubMenu2_OptionA, SubMenu2_OptionB, SubMenu2_OptionC, SubMenu2_OptionD, SubMenu2_OptionE var id: Int { rawValue } var name: String { switch self { case .SubMenu2_OptionA: return "Submenu2 Option A" case .SubMenu2_OptionB: return "Submenu2 Option B" case .SubMenu2_OptionC: return "Submenu2 Option C" case .SubMenu2_OptionD: return "Submenu2 Option D" case .SubMenu2_OptionE: return "Submenu2 Option E" } } } struct EnumProtTest: View { var body: some View { VStack { HStack { ForEach(MainMenu.allCases) { value in Text("\(value.name)") .padding() .background(Color.blue) } } HStack { TheContentView(data: MainMenu.One.submenu) { item in Text("\(item.name)") .padding() .background(Color.purple) } } HStack { TheContentView(data: MainMenu.Two.submenu) { item in Text("\(item.name)") .padding() .background(Color.purple) } } } } } // 简化约束后的TheContentView struct TheContentView<Data: RandomAccessCollection, ElementView: View>: View where Data.Element: OptionProtocol { var data: Data var itemView: (Data.Element) -> ElementView var body: some View { ForEach(data) { item in itemView(item) .padding() .background(Color.purple) } } }
关于你的菜单实现方案的小建议
用枚举实现固定结构的菜单是非常棒的选择!这种方案类型安全,所有菜单选项都是编译时确定的,不会出现运行时的类型错误,而且维护起来很方便——新增菜单选项只需要在枚举里加case就行,还能利用CaseIterable快速获取所有选项。如果你的菜单不会动态变化(比如从服务器获取),这个方案完全是最优解,放心用就好!
内容的提问来源于stack exchange,提问作者Aнгел

