While waiting for SE-0395, does anyone have a working solution for using @ObservedObject
with existential types?
For pure Swift protocols, couple times I've use SE-0352 Implicitly Opened Existentials with generic views. But by default SE-0352 fails to type-erase generic view type, and needs a helper function that would return AnyView
. And writing such helper functions requires repeating all the parameters of the view initialiser, which can be a lot of boiler-plate code:
struct MyView<ModelType: MyModel, AnotherModelType: AnotherModel>: View {
@ObservedObject var model: ModelType
@ObservedObject var anotherModel: AnotherModelType
var other: Bool
var params: String
var body: some View {
Text("\(model.data)")
}
}
func anyMyView(
model: some MyModel,
anotherModel: some AnotherModel,
other: Bool,
params: String
) -> AnyView {
AnyView(MyView(
model: model,
anotherModel: anotherModel,
other: other,
params: params
))
}
struct ContentView: View {
var myModel: any MyModel = MyModelImpl()
var anotherModel: any AnotherModel = MyModelImpl()
var body: some View {
// SE-0352 now works!
anyMyView(model: myModel, anotherModel: anotherModel, other: true, params: "abc")
}
}
Solving another problem - how to tunnel conformance to ObservableObject
through Objective-C code, I came up with another solution which allows using custom property wrapper @AnyObservedObject
with existentials of @objc
protocols. But only @objc
, because it relies on the compiler hack(?) for self-conformance of the @objc
protocols. Pure Swift protocols are not self-conformant even if they refine self-conformant @objc
protocol. Which makes sense. That would be a good use case for Existential subtyping as generic constraint.
@objc
protocol MyModel: CastableToAnyObservableObject {
var data: Int { get }
}
class MyModelImpl: MyModel, ObservableObject {
@Published
var data: Int = 42
// 1 line of boilerplate code
var asAnyObservableObject: AnyObservableObject { .get(for: self) }
}
struct MyView<ModelType: MyModel, AnotherModelType: AnotherModel>: View {
@AnyObservedObject var model: any ModelType
var body: some View {
Text("\(model.data)")
}
}
I do like the ergonomics of this solution - it seems to produce much less boiler-plate code than using generic views and implicitly opening existentials. So, I'm trying to find out a way to make it work for pure Swift types.
Are there any magical attributes that would allow pure Swift protocols to be self-conformable? Any other ways to express this requirement in the function signature?
If I cannot express this requirement, then I might get it work if I use a tuple of (any MyModel, AnyObservableObject)
as a type passed around. Actually the AnyObservedObject<any MyModel>
can already play this role - it is exactly that under the hood.
To finalise this idea, I need to find an ergonomic way to construct AnyObservedObject<any MyModel>
from MyModelImpl
. But to my best knowledge there is no way to express generic signature with parameters for MyModelImpl
and any MyModel
.
extension ObservableObject where Self.ObjectWillChangePublisher.Output == () {
// error: Type 'Self' constrained to non-protocol, non-class type 'P'
func wrap<P>() -> AnyObservedObject<P> where Self: P {
// Cannot convert value of type 'Self' to expected argument type 'P'
return AnyObservedObject(object: self, observable: .get(for: self))
}
}
let wrapped: AnyObservedObject<any MyModel> = MyModelImpl().wrap()