NEW: Check out my incredible new SpriteKit book! >>

How to fix “argument of #selector refers to instance method that is not exposed to Objective-C”

Written by Paul Hudson    @twostraws

Swift 4 changed the way Swift interacts with Objective-C in a way that will impact the code of most developers. Fortunately, there are a couple of fixes available, neither of which take too long to implement.

First, let’s take a look at what’s changed and why. In Swift 3 all methods from a class were automatically compiled so that they were available both to Swift code and to Objective-C code, which maximized compatibility.

However, that had a cost. Here’s what Doug Gregor wrote when proposing the change for Swift 4:

There is a cost for each Objective-C entry point, because the Swift compiler must create a "thunk" method that maps from the Objective-C calling convention to the Swift calling convention and is recorded within Objective-C metadata. This increases the size of the binary (preliminary tests on some Cocoa[Touch] apps found that 6-8% of binary size was in these thunks alone, some of which are undoubtedly unused), and can have some impact on load time (the dynamic linker has to sort through the Objective-C metadata for these thunks).

So, all these thunk methods had to be generated whether or not they were used, which wasn’t ideal. As of Swift 4, this has been dramatically scaled down so that these thunks are generated only when absolutely required, which means any time you write Swift code that needs to be called from Objective-C you will need to tell Swift what to do.

If you don’t think this happens often, here are some examples:

navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addSong))

let tap = UITapGestureRecognizer(target: self, action: #selector(userDoubleTapped))

let timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(chooseNewSong), userInfo: nil, repeats: true)

performSelector(inBackground: #selector(checkWikipedia), with: nil)

NotificationCenter.default.addObserver(self, selector: #selector(userLeavingApp), name: .UIApplicationWillResignActive, object: nil)

let lookup = UIMenuItem(title: "Applause", action: #selector(applaudGreatMusic))

undoManager?.registerUndo(withTarget: self, selector: #selector(undoPlaying), object: nil)

All seven of those examples ask some Objective-C code (e.g. NotificationCenter or Timer) to call our Swift code, which means all seven of those will stop working in Swift 4 unless you take action.

The error you’ll see looks like this: “Argument of '#selector' refers to instance method 'addSong()' that is not exposed to Objective-C,” and your code will refuse to build until you choose a solution. The key there is the #selector part: that’s the giveaway that you’ll need to use @objc with whatever method is being called.

So, that’s the problem and why it even exists. Let’s turn to the solutions, and there are two to choose from.

First, you can mark individual methods using the @objc attribute, like this:

@objc func myMethod() {
    // code
}

That instructs the Swift compiler to make an Objective-C thunk for that one method. This means you retain nearly all the performance benefits of the new Swift 4 approach – the thunk is generated only when needed.

The second option is to use the @objcMembers attribute on your whole class or struct, like this:

@objcMembers class ViewController: UIViewController {
    // code
}

That tells Swift to automatically generate Objective-C thunks for all methods in the class, so you don’t need to mark them individually using @objc.

Now, there are two important times when @objc isn’t needed:

  1. When you’re using @IBAction to connect an event from a storyboard. The @IBAction attribute automatically implies @objc, so you don’t need both.
  2. When you’re implementing a method from an Objective-C protocol, that automatically implies @objc because it doesn’t make sense otherwise.

Remember, if @objc is required but not present, Xcode will refuse to build your code – it’s not the kind of thing you can just forget.

Honestly, I think it’s sad that one of the world’s most progressive languages is having to look backwards like this, but it looks like we’re stuck with this change.

Available from iOS 2.0

Did this solution work for you? Please pass it on!

Other people are reading…

About the Swift Knowledge Base

This is part of the Swift Knowledge Base, a free, searchable collection of solutions for common iOS questions.

Master iOS 10 now!

My book Practical iOS 10 gives you seven complete coding projects that teach all the major new features in iOS 10 in a smart, practical way.

Click here to visit the Hacking with Swift store >>