robnik
1
I'm trying to display some SwiftUI Views for an object that is a member of a polymorphic hierarchy. For example, "markers" is a drawing/painting application. There are different kinds of markers with different properties - sizes, colors, etc.
I'd like the UI to conditionally display views, depending on the protocols supported by the model object. I run into problems with type casting...
protocol Marker : ObservableObject {
var name: String { get set }
}
protocol MarkerWithSize: Marker {
var size: Float { get set }
}
struct ContentView<MT: Marker>: View {
@ObservedObject var marker: MT
var body: some View {
VStack {
Text("name: \(marker.name)")
if marker is MarkerWithSize {
// This cast fails - XXX
MarkerWithSizeSection(marker: marker as! MarkerWithSize)
}
}
}
}
struct MarkerWithSizeSection<MT: MarkerWithSize>: View {
@ObservedObject var marker: MT
var body: some View {
Slider(value: $marker.size, in: 1...50)
}
}
That cast fails with the notorious error:
Protocol type 'MarkerWithSize' cannot conform to 'MarkerWithSize' because only concrete types can conform to protocols
I need some way to get a binding to the properties in the sub-protocol:
$marker.size
To use SwiftUI things like
Slider(value: $marker.size, in: 1...5)
I can refer to $marker.name in the ContentView, but not $marker.size. The $marker there is (according to Xcode) of type ObservableObject<MT>.Wrapper.
What to do in this situation?
Lantua
2
You can remove the concrete MT since it doesn’t seem to be of use.
struct MarkerWithSizeSection: View {
@ObservedObject var marker: MarkerWithSize
var body: some View {
Slider(value: $marker.size, in: 1...50)
}
}
robnik
3
That doesn't compile:
Property type 'MarkerWithSize' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
I don't think you can use bare protocol types with @ObservedObject. That's why I needed the generic type parameter in ContentView too.
somu
(somu)
4
Only classes can conform to ObservableObject (because of AnyObject requirement)
- So the model would be a
class.
- You could create base class called
BaseMarker and use it in the View
- You could pass in subclasses of
BaseMarker and things would work as shown below:
Model:
class BaseMarker : ObservableObject{
@Published var name : String
@Published var size : Float
init(name: String,
size: Float) {
self.name = name
self.size = size
}
}
class MarkerA : BaseMarker {
@Published var color : String
init(name: String,
size: Float,
color: String) {
self.color = color
super.init(name: name,
size: size)
}
}
View:
struct ContentView: View {
@ObservedObject var marker: BaseMarker
var body: some View {
VStack {
Text("name: \(marker.name)")
MarkerWithSizeSection(marker: marker)
}
}
}
struct MarkerWithSizeSection: View {
@ObservedObject var marker: BaseMarker
var body: some View {
Slider(value: $marker.size, in: 1...50)
}
}
Initialising the view:
let marker = MarkerA(name: "marker A", size: 10, color: "green")
let contentView = ContentView(marker: marker)
robnik
5
Okay, I can make it work with classes, but that's disappointing. I read all this stuff about protocols and protocol oriented programming, but then can't use it with models in SwiftUI?
Sometimes I create protocols that are only meant for classes. You can mark a protocol as such. protocol P : class { ... }
Your example doesn't quite fit my question, because you didn't put size in the subclass. I was surprised that when I did that, the example didn't work. The Slider binding won't update the size. Apparently @Published doesn't work in subclasses. But a simple workaround is to manually send the Combine event. In other words, this failed:
class MarkerA : BaseMarker {
@Published var size: Float
But this worked:
class MarkerA : BaseMarker {
var size : Float {
willSet {
objectWillChange.send()
}
}
I wonder if that's a bug with @Published.