Reputation: 4573
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
Reputation: 73186
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:
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 theobjc
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