Very strange override behavior

I have the following setup in Xcode 10 beta 5, in a Mac app.

Basically the following. I want to implement a delegate method from the NSTextFieldDelegate protocol. (Actually its super-protocol NSControlTextEditingDelegate)

If I try:

class ViewController: NSViewController, NSTextFieldDelegate {

     func controlTextDidChange(_ obj: Notification) {
          // code here
     }
}

I get an error that I need the override keyword.

If I add that keyword, I would also want to call super to make sure I don't exclude any superclass functionality:

class ViewController: NSViewController, NSTextFieldDelegate {

     override func controlTextDidChange(_ obj: Notification) {
          super.controlTextDidChange(obj)
          // code here
     }
}

With this text, there is an exception at runtime that the superclass does not have a method controlTextDidChange(_:).

How can you be forced to override a method that doesn't exist? Any suggestions or thoughts on what is going on here?

(I realize the workaround is to use 'override' and not call super, but something seems wrong somewhere for it to be like this.)

1 Like

The answer to this one is based in Objective-C history: in the first version of Objective-C protocols, requirements couldn't be optional. So if you wanted to declare that a method might be present on your delegate…

…you declared it on NSObject.

@interface NSObject(NSControlSubclassNotifications)
- (void)controlTextDidBeginEditing:(NSNotification *)obj;
- (void)controlTextDidEndEditing:(NSNotification *)obj;
- (void)controlTextDidChange:(NSNotification *)obj;
@end

In this case it would probably have been reasonable to put an empty implementation NSObject, but the AppKit engineers of 15? 20? years ago went with another decision: check at runtime if the method is actually present on the delegate. This is better in some ways and worse in others; I won't go into it here.

Times change. Objective-C got @optional protocol methods and all of these "informal protocols" (as they were known) became actual types—in this case NSControlTextEditingDelegate. Many years after that Swift comes along with the rule that methods that match a method declared on a superclass need to use override. But no one's removed the declarations from NSObject from way back, since that could be a source compatibility problem for anyone who's not using NSControlTextEditingDelegate. (And now it would be a source compatibility break in Swift too, since that override would have to go away.)

And that's how we end up here, where the compiler makes you write override even though the base class doesn't implement the method at run time. From Swift's perspective, the Objective-C header is lying about what NSObject implements; from Objective-C's perspective, Swift made it awkward to use a 20-year-old pattern, even one that has better alternatives these days.

13 Likes

[What Jordan wrote plus…] Over the last N releases various framework teams have been converting these informal protocols to the formal protocols you’d expect as a Swift developer. In fact, there’s been a bunch of changes in this space in the Xcode 10 SDKs. For more context see this DevForums thread.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

Thank you both for the explanations.

Overall, Obj-C and Swift interoperability has been very well done, especially considering the languages have very different opinions regarding strict type safety.

It's what makes edge cases like this stand out and seem so odd.

@eskimo Do you know if the code migrator will get rid of the override designations when they are no longer needed?

Thanks again.

Do you know if the code migrator will get rid of the override
designations when they are no longer needed?

It does. Here’s what I did to test this:

  1. I created a new Cocoa app project using Xcode 9.4.

  2. I added the following to the ViewController class:

     override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
         return true
     }
    

    .

  3. I opened that project in 10.0b6.

  4. It built without warnings in compatibility mode.

  5. I upgraded it to Swift 4.2, and now the method looks like this:

     func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
         return true
     }
    

That’s cool. The one gotcha is that it did not changed the ViewController class to conform to NSMenuItemValidation. That seems bugworthy to me.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like