I'm trying to create a base Node
class that can be extended with inheritance via a composable set of protocols to add additional features conditionally to the base Node
class. To start, I'm adding a Previewable
protocol that can extend a Node
to provide an optional preview view to some implementation.
Self contained example:
import SwiftUI
// MARK: Base node protocol
protocol Node: AnyObject {
var name: String { get }
}
extension Node {
var name: String {
get { "Unnamed Node" }
}
}
// MARK: Previewable node
protocol Previewable {
associatedtype PreviewBody: SwiftUI.View
@ViewBuilder var previewBody: Self.PreviewBody { get }
}
extension Node {
var previewBody: some View {
Text("No Preview")
}
}
// MARK: Test node impl
class TestNode: Node, Previewable {
typealias PreviewView = Text
var name: String = "Test Node"
var previewBody: some View {
Text("Test Preview")
}
}
// MARK: Test content view
struct NodeView<NodeType: Node>: View {
var node: NodeType
init (_ node: NodeType) {
self.node = node
}
var body: some View {
VStack {
node.previewBody
}
}
}
struct ContentView: View {
var node = TestNode()
var body: some View {
VStack {
NodeView(node)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
My assumption was that the previewBody
implementation on the TestNode
type would override the Node
extension that returns No Preview
by default. However when I'm running this, I'm seeing "No Preview"
instead of the "Test Preview"
text provided by the class implementation.
I'm getting a feeling that I'm tackling this the wrong way but I'm not entirely sure how to correct it. Ideally I'd like the Previewable
type to be implemented in a way that Node
inheriting types can be checked for optional compliance when they're displayed. I can't figure out a way to do it though without generics (i.e. in the NodeView
struct) that would allow NodeView
to just take a generic node as an argument (later I need a method of passing an array of [any Node]
to a view to display them all depending on what protocols they implement, so generics in this case isn't ideal).
How can I update this better conform to Swift semantics and make it correctly display the implementation's "Test Preview"
view and not the extension's default "No Preview"
view?
CodePudding user response:
As Alexander stated in the comments,
Methods on protocols aren't dynamically dispatched unless they're requirements on the protocol themselves.
When you do node.previewBody
, Swift only knows that node
is some Node
. The only thing called previewBody
declared on Node
is the one with the Text("No Preview")
. The Text("Test Preview")
one is declared in TestNode
, and Swift doesn't know that node
is TestNode
at compile time.
Compare this to when you access node.name
, which is dispatched dynamically. There are two things called name
declared on Node
- the property requirement and the default implementation that returns "Unnamed Node"
. Swift picks the former so that at runtime, it is dynamically dispatched to the witness of the protocol requirement.
I think your design would make more sense if Previewable
provided the default implementation anyway:
extension Previewable {
var previewBody: Text {
Text("No Preview")
}
}
And since NodeView
uses previewBody
, it should require that the NodeType
be both a Node
and Previewable
.
struct NodeView<NodeType: Node & Previewable>: View {