3

Why do Objective-C APIs like NotificationCenter.addObserver or UIButton.addTarget get imported into Swift with a an observer of type Any, instead of AnyObject?

Is there any reason for that? Is it even possible to correctly specify selector to something except @objc method of class?

2 Answers2

4

The reasoning for this is explained in Objective-C id as Swift Any. In Swift 2, it was as you suggest. id parameters were bridged as AnyObject. In Swift 3, they were changed to Any:

This change makes Objective-C APIs more flexible in Swift, because Swift-defined value types can be passed to Objective-C APIs and extracted as Swift types, eliminating the need for manual “box” types. These benefits also extend to collections: Objective-C collection types NSArray, NSDictionary, and NSSet, which previously only accepted elements of AnyObject, now can hold elements of Any type.

The change was detailed in SE-0116 Import Objective-C id as Swift Any type.

This made Swift/ObjC interop dramatically simpler than in Swift 2, but it did create some specific cases, like the targets of actions, where it was possibly a little easier to create code that would fail at runtime. In practice, though, the limitation on the selector parameter is the more important protection there. Mismatching target and selector have always been a risk in ObjC, and allowing Any here doesn't actually make it much worse. You still need to use #selector correctly, and it will throw errors if you reference non @objc methods.

Joakim also makes some good arguments, though I would typically prefer that Observer require NSObjectProtocol in this case. I suspect it was more a matter of not being worth the trouble to add NS_REFINED_FOR_SWIFT macros for these cases. If you find real-world errors occurring due to the lack of AnyObject enforcement, that would be worth bringing up on the Swift forums as a possible improvement.

But to your secondary question "is it even possible to correctly specify selector to something except @objc method of class," the answer is yes, if by "correctly" you mean "it will work."

As an example, you can send selectors to String, even though it is a struct, by referencing methods attached to NSString:

let string: String = "This is a Swift string"

// This is an extension on **NSString**, not String.
extension NSString {
    @objc func strangeButTrue() { print("Yes, this will print") }
}

Timer.scheduledTimer(timeInterval: 1, 
                     target: string, 
                     selector: #selector(NSString.strangeButTrue),
                     userInfo: nil, 
                     repeats: false)

Note also that #selector(NSString.strangeButTrue) is a compile-time convenience. This can also be expressed this way:

let nameOfSelectorDecidedAtRunTime = "strangeButTrue"
Timer.scheduledTimer(timeInterval: 1,
                     target: string,
                     selector: Selector(nameOfSelectorDecidedAtRunTime),
                     userInfo: nil,
                     repeats: false)

Constructing selectors at runtime from strings is somewhat common in ObjC. Many large projects I've worked on have at least one place it's done. Being able to treat bridged objects this way does make it easier to port existing ObjC code, even if this isn't something you should really do much in Swift.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
2

One reason is that you could use a protocol type for the object and the protocol might be conformed to by both reference and value types.

An example

protocol Observer {
    func observe()
}

class LookOut: Observer {
    @objc func observe() {}
}

This will work since the parameter is of type Any

let observer: Observer = LookOut()

NotificationCenter.default.addObserver(observer,
                                       selector: #selector(LookOut.observe),
                                       name: .NSCalendarDayChanged,
                                       object: nil)

But with this method it will fail since the protocol doesn't conform to AnyObject

func fakeObserver(_ object: AnyObject, selector: Selector) {}

So

fakeObserver(observer, selector: #selector(LookOut.observe)) 

generates the error

Argument type 'any Observer' expected to be an instance of a class or class-constrained type

And a value type like below will of course not work in either case since the function can't be annotated with @objc

struct LookIn: Observer {
    func observe() {}
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52