带关联类型的协议无法作为@Published属性类型的原因问询
我有多个遵循HumanProtocol协议的数据模型(struct),想在ViewModel中用@Published属性存储任意一个该协议的实例,并且在UI中访问其属性赋值,但编译器提示无法完成类型检查,请问这个方案不可行的原因是什么?
相关代码如下:
protocol HumanProtocol { associatedtype Human }
struct Female: HumanProtocol { typealias Human = Female var name:String } struct Male: HumanProtocol { typealias Human = Male var name:String }
class HumanViewModel:ObservableObject { @Published var selectedHuman: Any HumanProtocol? init(selectedHuman:Any HumanProtocol) { self.selectedHuman = selectedHuman } }
期望实现效果:
@StateObject var humanVM = HumanViewModel(selectedHuman:Male(name:"")) TextField("Nickname", text: $humanVM.selectedHuman.name ) .modifier(TextFieldModifier(width: 250, height: 30)) .padding(.top)
核心原因
带关联类型的协议无法直接作为变量类型
HumanProtocol定义了associatedtype Human,这类协议属于「存在类型协议」,Swift不允许直接用Any HumanProtocol声明变量——关联类型会让协议的具体类型在编译期无法确定,编译器没法提前知道selectedHuman到底是Male还是Female,自然无法解析它的name属性。可选值+协议属性无法直接生成SwiftUI绑定
就算解决了类型问题,selectedHuman是可选类型,直接写$humanVM.selectedHuman.name会因为可选链的存在,无法生成有效的Binding——SwiftUI的绑定需要明确的、可读写的属性路径,可选值的属性没法直接满足这个要求。
可行解决方案
方案一:简化协议(最直接)
如果业务上不需要关联类型,直接把name属性提到协议里,改成普通协议:
protocol HumanProtocol { var name: String { get set } } struct Female: HumanProtocol { var name: String } struct Male: HumanProtocol { var name: String }
ViewModel里用Swift 5.7+支持的any关键字声明协议类型:
class HumanViewModel: ObservableObject { @Published var selectedHuman: any HumanProtocol init(selectedHuman: any HumanProtocol) { self.selectedHuman = selectedHuman } }
然后在ViewModel里加一个绑定计算属性,解决UI绑定问题:
var nameBinding: Binding<String> { Binding( get: { self.selectedHuman.name }, set: { self.selectedHuman.name = $0 } ) }
UI中使用这个绑定:
TextField("Nickname", text: humanVM.nameBinding)
方案二:用枚举统一包装类型
如果需要区分不同的Human类型,或者必须保留原有的协议结构,用枚举把所有遵循协议的类型包起来:
enum Human { case male(Male) case female(Female) var name: String { get { switch self { case .male(let male): return male.name case .female(let female): return female.name } } set { switch self { case .male(var male): male.name = newValue self = .male(male) case .female(var female): female.name = newValue self = .female(female) } } } }
ViewModel改成使用这个枚举类型:
class HumanViewModel: ObservableObject { @Published var selectedHuman: Human init(selectedHuman: Human) { self.selectedHuman = selectedHuman } }
初始化和UI绑定就可以直接用了:
@StateObject var humanVM = HumanViewModel(selectedHuman: .male(Male(name:""))) TextField("Nickname", text: $humanVM.selectedHuman.name )
方案三:类型擦除包装器(适合复杂场景)
如果必须保留带关联类型的协议,不想用枚举,可以写一个类型擦除的结构体来包装实例:
protocol HumanProtocol { associatedtype Human var name: String { get set } } struct AnyHuman<H>: HumanProtocol { typealias Human = H private let _getName: () -> String private let _setName: (String) -> Void var name: String { get { _getName() } set { _setName(newValue) } } init<T: HumanProtocol>(_ human: T) where T.Human == H { _getName = { human.name } _setName = { human.name = $0 } } }
ViewModel中使用擦除后的类型:
class HumanViewModel: ObservableObject { @Published var selectedHuman: AnyHuman<Any>? init<T: HumanProtocol>(selectedHuman: T) { self.selectedHuman = AnyHuman(selectedHuman) } }
不过这种方式还是需要处理可选值的绑定问题,不如前两种方案简洁。
内容的提问来源于stack exchange,提问作者David.C

