Can't use protocols with SwiftUI models?

I've been using protocols to help model a hierarchy of different object types. As I try to convert my app to use SwiftUI, I'm finding that protocols don't work with the ObservableObject that you need for SwiftUI models. I wonder if there are some techniques to get around this, or if people are just giving up on "protocol oriented programming" when describing their SwftUI models? There is example code below. The main problem is that it seems impossible to have a View that with an model of protocol `P1` that conditionally shows a subview with more properties if that model also conforms to protocol `P2`.


For example, I'm creating a drawing/painting app, so I have "Markers" which draw on the canvas. Markers have different properties like color, size, shape, ability to work with gradients. Modeling these properties with protocols seems ideal. You're not restricted with a single inheritance class hierarchy. But there is no way to test and down-cast the protocol...


protocol Marker : ObservableObject {
    var name: String { get set }
}

protocol MarkerWithSize: Marker {
    var size: Float { get set }
}

class BasicMarker : MarkerWithSize {
    init() {}
    @Published var name: String = "test"
    @Published var size: Float = 1.0
}
struct ContentView<MT: Marker>: View {
    @ObservedObject var marker: MT
    var body: some View {
        VStack {
            Text("Marker name: \(marker.name)")
            if marker is MarkerWithSize {
                // This next line fails
                // Error: Protocol type 'MarkerWithSize' cannot conform to 'MarkerWithSize' 
                //        because only concrete types can conform to protocols
                MarkerWithSizeSection(marker: marker as! MarkerWithSize)
            }
        }
    }
}
struct MarkerWithSizeSection<M: MarkerWithSize>: View {
    @ObservedObject var marker: M
    var body: some View {
        VStack {
            Text("Size: \(marker.size)")
            Slider(value: $marker.size, in: 1...50)
        }
    }
}


Thoughts?

Post not yet marked as solved Up vote post of rnikander Down vote post of rnikander
8.0k views

Replies

Making your Marker type vend its view works:


protocol Marker : ObservableObject {
    associatedtype Body: View

    var name: String { get set }
    var body: Body { get }
}

protocol MarkerWithSize: Marker {
    var size: Float { get set }
}

class UnsizedMarker: Marker {
    init() {}
    @Published var name: String = "UnsizedMarker"

    var body: some View {
        VStack {
            Text("Marker name: \(name)")
        }
    }
}

class BasicMarker : MarkerWithSize {
    init() {}
    @Published var name: String = "test"
    @Published var size: Float = 1.0

    var body: some View {
        VStack {
            Text("Marker name: \(name)")
            Text("Size: \(size)")
            Slider(value: Binding(get: {self.size}, set: {self.size=$0}),
                   in: 1...50)
        }
    }
}

struct ContentView<MT: Marker>: View {
    @ObservedObject var marker: MT
    var body: some View {
        marker.body
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            ContentView(marker: UnsizedMarker())
            Divider()
            ContentView(marker: BasicMarker())
        }
    }
}


I'm inclined to think that this situation isn't quite a fit for the protocol approach, though. Since you're relying on ObservableObject at the root of the protocol tree, you're already limited to class types, which means you can just use classes all the way and use inheritance to do what you need. Also, your sample at least is declaring two inheriting protocols, which further suggests that classes are the way to go: protocols are usually used to manage several disparate sets of requirements, such as Equatable and CustomStringConvertible.


Here's a class-based approach that works:


class Marker: ObservableObject {
    @Published var name: String = "Marker"
}

class MarkerWithSize: Marker {
    @Published var size: Float = 1.0

    override init() {
        super.init()
        self.name = "SizedMarker"
    }
}

struct ContentView: View {
    @ObservedObject var marker: Marker
    var body: some View {
        VStack {
            Text("Marker name: \(marker.name)")
            if marker is MarkerWithSize {
                MarkerWithSizeSection(marker: marker as! MarkerWithSize)
            }
        }
    }
}

struct MarkerWithSizeSection: View {
    @ObservedObject var marker: MarkerWithSize
    var body: some View {
        VStack {
            Text("Size: \(marker.size)")
            Slider(value: $marker.size, in: 1...50)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            ContentView(marker: Marker())
            Divider()
            ContentView(marker: MarkerWithSize())
        }
    }
}
  • this solution doesn't work, since SwiftUI does not reflect changes published on the super (as ObservedObject) since the models are nested, so it does compile but at runtime it will not refresh the view

    in your example the change occur on the sub model, but if a change would occur on the super then the view will not re-render

Add a Comment

Interesting. At first glance here I think I can make that work. I will have to think about pros and cons between that and the class-based approach in the real app.


Thank you for the response.


You mentioned that maybe protocols aren't a good fit here:


> [quote] I'm inclined to think that this situation isn't quite a fit for the protocol approach, though. Since you're relying on ObservableObject at the root of the protocol tree, you're already limited to class types, which means you can just use classes all the way and use inheritance to do what you need.


I'm attracted to protocols in this situation because as I think of different features that a marker might support, I can imagine concrete classes that support different subsets of those features. It doesn't seem to fit into a neat single inheritance hierarchy. So I was thinking I could use protocols somewhat like a 'mixin' or 'trait'. I haven't gotten deeply into this though. At the moment I don't have many marker types, so I'm not sure how it will play out.

One approach that's used throughout SwiftUI is the concept of a proxy type. For instance, scroll views internally use a proxy type to read and write things like the content offset. It may be possible for your generic Marker type to vend optional proxy objects for different features, and only the types that support a particular setting provide that type of proxy:


struct SizeProxy {
    @Binding size: Double
}

...

Text(marker.name)

if marker.sizeProxy != nil {
    Slider(marker.sizeProxy!.$size, ...)
}