Problems with ObservableObject, bindings, and protocol inheritance

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?

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)
    }
}

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.

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)

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.