Home > database >  How to override a parent's class extension with child definition in Swift
How to override a parent's class extension with child definition in Swift

Time:09-27

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 {
  • Related