Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parent-child protocol relationship and ObjC availability

I can't get the following code to work:

@objc protocol Child { }

@objc protocol Parent {
    var child: Child { get }
}
    
class ChildImpl: Child {
    // not part of the `Child` protocol
    // just something specific to this class 
    func doSomething() { }
}
    
class ParentImpl: Parent {
    let child = ChildImpl()

    func doSomething() {
        // need to be able to access `doSomething`
        // from the ChildImpl class
        childImpl.doSomething()
    }

    // this would solve the problem, however can't access the ChildImpl members
    // that are not part of the protocol
    // let child: Child = ChildImpl()
    // as well as this, however maintaining two properties is an ugly hack
    // var child: Child { return childImpl }
    // private let childImpl = ChildImpl()
}

The error I get:

Type 'ParentImpl' does not conform to protocol 'Parent'.
Do you want to add protocol stubs?

Basically I have two parent-child protocols, and two classes that implement the two protocols. But still, the compiler doesn't recognize that that ChildImpl is a Child.

I can make the errors go away if I use an associated type on Parent

protocol Parent {
    associatedtype ChildType: Child
    var child: ChildType { get }
}

, however I need to have the protocols available to Objective-C, and also need to be able to reference child as the actual concrete type.

Is there a solution to this that doesn't involve rewriting the protocols in Objective-C, or doesn't add duplicate property declarations just to avoid the problem?

like image 727
Cristik Avatar asked Jan 03 '17 11:01

Cristik


2 Answers

I referred in the comments a link showing what you've tried, using associated type or separate property just to fulfil protocol conformance. I thing Swift will soon support inferring type from composed types like let child: Child & ChildImpl = ChildImpl() or simply child: ChildImpl since ChildImpl is Child. But until then thought I suggest one more alternative which is to separate the apis you need from ChildImpl and put them in a separate protocol to which Child inherits. This way when Swift compiler supports this feature, you don't need to refactor but simply remove it.

// TODO: Remove when Swift keeps up.
@objc protocol ChildTemporaryBase { }
private extension ChildTemporaryBase {
    func doSomething() {
        (self as? ChildImpl).doSomething()
    }
}

@objc protocol Child: ChildTemporaryBase { }

class ParentImpl: Parent {
    let child: Child = ChildImpl()
    func testApi() {
        child.doSomething?()
    }
}
like image 171
Lukas Avatar answered Oct 19 '22 15:10

Lukas


What you're trying to do is called covariance and swift does not support covariance in protocols or classes/structs conforming to those protocols. You either have to use Type-Erassure, or associatedTypes:

protocol Child { }

protocol Parent {
    associatedtype ChildType: Child
    var child: ChildType { get }
}

class ChildImpl: Child {
    func doSomething() {
        print("doSomething")
    }
}

class ParentImpl: Parent {
    typealias ChildType = ChildImpl
    let child = ChildImpl()

    func test() {
        child.doSomething()
    }
}
ParentImpl().test() // will print "doSomething"

And here's the Type-Erased Parent for general usage of Parent protocol:

struct AnyParent<C: Child>: Parent {
    private let _child: () -> C
    init <P: Parent>(_ _selfie: P) where P.ChildType == C {
        let selfie = _selfie
        _child = { selfie.child }
    }

    var child: C {
        return _child()
    }
}

let parent: AnyParent<ChildImpl> = AnyParent(ParentImpl())
parent.child.doSomething() // and here in Parent protocol level, knows what is child's type and YES, this line will also print "doSomething"
like image 43
farzadshbfn Avatar answered Oct 19 '22 14:10

farzadshbfn