Frederik Winkelsdorf
Frederik Winkelsdorf

Reputation: 4573

Issue with the call hierarchy of a swift protocol extension

I've implemented a little wrapper for Alamofire's reachability. Now I ran into an issue with a subclass not receiving notifications.

As dfri added in a comment, the problem can be described as a superclass method invoked by a subclass instance does not see an overridden subclass method if it is placed in an extension.

Thanks for his excellent gist showing the reproducible problem with the usual foo/bar: https://gist.github.com/dfrib/01e4bf1020e2e39dbe9cfb14f4165656

The protocol & extension logic:

protocol ReachabilityProtocol: class {    
    func reachabilityDidChange(online: Bool)
}

extension ReachabilityProtocol {

    func configureReachabilityManager() {
        // triggered externally:
        rechabilityManager?.listener = { [weak self] status in
            self?.reachabilityDidChange(online)
        }
    }

    func reachabilityDidChange(online: Bool) {
        // abstract, implement in subclasses
    }

}

Now ViewController A adopts this protocol and implements the method:

class A: UIViewController, ReachabilityProtocol {

    func reachabilityDidChange(online: Bool) {
        debugPrint("123")
    }

}

Working until here, prints 123.

ViewController B is a subclass and overrides the method from A:

class B: A {

    override func reachabilityDidChange(online: Bool) {
        super.reachabilityDidChange(online)
        debugPrint("234")
    }

}

Working, too. Prints now 234 and 123 upon notification.

Now the tricky part, in terms of structuring the code, the overriden method got moved into an own extension inside of B (same swift file as class B):

class B: A {
...
}

// MARK: - Reachability Notification
extension B {

    override func reachabilityDidChange(online: Bool) {
        super.reachabilityDidChange(online)
        debugPrint("234")
    }

}

Now B's reachabilityDidChange() isn't called anymore. Output is just 123.

To check the logic I even tried to remove the override keyword: the compiler immediately complains about needing it.

So from what I can say the call hierarchy is correct. Visibility too.

Subsumption: If the method overridden is placed into it's own extension, it's not visible/invoked anymore but the compiler still requires the override keyword.

That's something I didn't came across yet, or it's me working to long on the same project today..

Any hints?

Upvotes: 1

Views: 398

Answers (1)

dfrib
dfrib

Reputation: 73186

Overriding superclass methods in extensions: only allowed for Objective-C compatible methods

First of all we note that you can only override a superclass method in an extension of a subclass if the method is Objective-C compatible. For classes deriving from NSObject, this is true for all instance methods (which is true here, as UIViewController derives from NSObject). This is covered in e.g. the following Q&A:


Enforcing dynamic dispatch via Objective-C runtime

Now, from Interoperability - Interacting with Objective-C APIs - Requiring Dynamic Dispatch, we note the following

When Swift APIs are imported by the Objective-C runtime, there are no guarantees of dynamic dispatch for properties, methods, subscripts, or initializers. The Swift compiler may still devirtualize or inline member access to optimize the performance of your code, bypassing the Objective-C runtime.

You can use the dynamic modifier to require that access to members be dynamically dispatched through the Objective-C runtime.

Moreover, from The Language Ref. - Declarations - Declaration Modifyers we read

dynamic (modifier)

Apply this modifier to any member of a class that can be represented by Objective-C. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched using the Objective-C runtime. Access to that member is never inlined or devirtualized by the compiler.

Because declarations marked with the dynamic modifier are dispatched using the Objective-C runtime, they’re implicitly marked with the objc attribute.

Enforcing dynamic dispatch for reachabilityDidChange(...) in A

Hence, if you add the dynamic modifier to method reachabilityDidChange(...) in your superclass A, then access to reachabilityDidChange(...) will always be dynamically dispatched using the Objective-C runtime, and hence find and make use of the correct overridden reachabilityDidChange(...) method in the class B extension, for instances of B. Hence,

dynamic func reachabilityDidChange(online: Bool) { ... } 

in A will fix the above.


Below follows a more minimal example of your issue above, redeemed by demanding dynamic dispatch via obj-c runtime for method foo() in class A (equivalent to your method reachabilityDidChange(...) in class A).

import UIKit

protocol Foo: class {
    func foo()
}

extension Foo {
    func foo() { print("default") }
}

class A: UIViewController, Foo {
    dynamic func foo() { print("A") } // <-- note dynamic here
    func bar() { self.foo() }  
            /*          \
                   hence, foo() is dynamically dispatched here, and for
                   instances where "self === B()", this is correctly
                   resolved as the foo() in the extension to B          */
}

class B : A { }

extension B {
    override func foo() {
        super.foo()
        print("B")
    }
}

let a = A()
a.bar() // A

let b = B()
b.bar() // A B
        /* or, only "A" if not using dynamic dispatch */

Upvotes: 3

Related Questions