9
votes

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?

6
See this Q&A – one feasible (but not particularly nice) solution in your case would be to define a dummy property of type Child! to ParentImpl to satisfy the protocol requirement (and then have your actual property be of type ChildImpl!). - Hamish
@Hamish, I evaluated that approach also, however (as you said) it's not very nice, and it requires maintaining two properties with the same role :( - Cristik
@Cristik Yeah :/ Unfortunately, I think it's probably the best you're going to be able to manage until Swift supports it – although I hope someone can prove me wrong with a better workaround. - Hamish
I find this to be a bug in Swift's compiler tbh - Mihai Fratu

6 Answers

2
votes

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?()
    }
}
2
votes

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"
0
votes

If you don't mind adding extension property to ParentImpl class:

@objc protocol Child {}

@objc protocol Parent {
    var child: Child! { get }
}

class ChildImpl: Child { }

class ParentImpl: Parent {
    var child: Child!
}

extension ParentImpl {

    convenience init(child: Child?) {
        self.init()
        self.child = child
    }

    var childImpl: ChildImpl! {
        get { return child as? ChildImpl }
        set { child = newValue }
    }

}

let parent = ParentImpl(child: ChildImpl())
let child = parent.child
0
votes

Your ParentImpl class hasn't Child type protocol. I solved that this solution.

class ParentImpl: Parent {
   var child: Child = ChildImpl()
}
0
votes

One almost-good solution that I found, now that we have Swift 5.1, is by using a property wrapper:

@propertyWrapper
struct Hider<T, U> {
    let wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }

    var projectedValue: U { return wrappedValue as! U }
}

@objcMembers class ParentImpl: NSObject, Parent {
    @Hider<Child, ChildImpl> var child = ChildImpl()
}

This way child is exposed as Child, and $child is exposed as ChildImpl, which allows usage of non-protocol members from within ParentImpl.

The solution is not ideal, as I could not yet find a way to describe that T should be a super-type for U.

0
votes

Short answer is NO, the current design doesn't work with the current Swift language.

The reason is that the var child: Child { get } protocol requirement creates behind the scenes an existential container, something like ExistentialContainer<Child>. And due to this, any type conforming to the protocol must also declare an existential container of the same type, and not ExistentialContainer<Subtype>.

Seems that existential containers are not covariant, so for the time being one needs to use the workarounds described in the question and the other answers.