UIButton extension method not called

Hello!

Encountered a potential bug in iOS 14 but wanted to cross-check here before creating a feedback on Feedback Assistant.

I have a mixed ObjC/Swift project, with the majority of ViewControllers currently written in ObjC.
There are a few extensions/categories in Swift/ObjC that add convenience methods to various UI elements, such as UIButton.

One such extension on UIButton looks like this:

Code Block swift
extension UIButton {
    @objc func setTitle(_ title: String?) {
        self.setTitle(title, for: .normal)
        self.setTitle(title, for: .highlighted)
        self.setTitle(title, for: .selected)
    }
}

Now, I'm not sure if this somehow shadows a (upcoming?) method on UIButton, but this worked on iOS 13.

Calling this method, both from Swift or Objective-C, however does not set the title on iOS 14. It stays blank/on the default.

If I set a breakpoint inside that setTitle() method, it won't get hit either.

Removing the @objc from the method seems to fix the issue (for Swift-based VCs), but makes it impossible to call from Objective-C.
Moving the @objc to the top of the extension doesn't help either.

Is this a regression, or am I missing something? Would appreciate everyone's advice here.

If it is a bug I will of course file a feedback on Feedback Assistant and link it here.
Made some test with your code extension.

Effectively, does not work (in iOS 13) with @objc

I changed the name of the func as setTitleAll.

Code Block
extension UIButton {
@objc func setTitleAll(_ title: String?) {
print("Set title") // Just to see it's called
self.setTitle(title, for: .normal)
self.setTitle(title, for: .highlighted)
self.setTitle(title, for: .selected)
}
}


Then it works.

I'm not very fluent in objc, but seems that using the same name with different argument list creates the confusion.

@Claude31

Are you sure?

I've just ran it on a simulator device running iOS 13 and it works.. (on Xcode 12 beta).
This extension has been in the code base for a while now, probably 2-3 years, so I would be surprised if it hadn't worked on iOS 13.

Weirdly enough, if I stop app from Xcode and then reopen it on the simulator (without Cmd+R) the text shows up on an iOS 14 device too..
Yes, I just tested now, with XCode 11.5 and iOS 13 simulators.

@objc setTitle()
is not called
Changing to
@objc setTitleAll()
it is called

Did you try it with iOS 14 ?
Hmmm... There seems to be something that has changed between iOS 13.1 and iOS 13.5.

It works on iOS 13.1, but doesn't on iOS 13.5
I tested on iOS 13.5 simulator. In Swift code.
What does not work in 13.5 ?

It does work with
         
Code Block
@objc func setTitleAll(_ title: String?)

as well as with
Code Block
func setTitleAll(_ title: String?)


It works as well with a different signature for setTitle
          func setTitle(title: String?)

That seems to confirm my hypothesis:
  • with @objc, the signature with similar start

Code Block
@objc func setTitle(_ title: String?)


is not differentiated form
Code Block
func setTitle(_ title: String?, for state: UIControl.State)


The original setTitle is called, not the one defined in extension

but if you remove the _ or change the name, the extension is called.
I took a look.

It seems like this behaviour changed in iOS 13.4.
Calling the extension method unchanged works on iOS 13.0, 13.1, 13.2 and 13.3.

Running on 13.4 and 13.5 the same behaviour is observed as on iOS 14.

Something I've noticed though is that, if you terminate the app with the Stop button in Xcode and then restart the app by hand in the simulator (clicking its icon), the method works, both on iOS 13.4 and 13.5 and iOS 14.

So I'm not quite sure if this is really an OS issue, but rather an issue with Xcode or the Simulator?
The method works, you mean ?

Code Block
extension UIButton {
@objc func setTitle(_ title: String?) {
self.setTitle(title, for: .normal)
self.setTitle(title, for: .highlighted)
self.setTitle(title, for: .selected)
}
}


Once you have restarted app, does it work directly next time you compile ?

What is to be noticed, in my case:
  • run in 13.5

  • extension @objc func setTitle( title: String?) is not called

Stopping and restarting does not make it work.
  • extension @objc func setTitleAll(

title: String?) is called
  • extension @objc func setTitle( title: String?, dummy: Int) is called

I also tested on 13.0
  • @objc func setTitle(_ title: String?) runs OK

So, it seems effectively something has changed in 13.5.

I'll have to check next week, but yes. If you terminate the app and open it by hand in the simulator it works. Very weird behavior. I'll have to look into it further next week.
So I had another look this morning.

Running the app from Xcode 11.5 (11E608c) in an iPhone 11 iOS 13.4 simulator, the method in the extension is not called. However, if I terminate the app from Xcode (via the Stop button) and then relaunch it myself directly in the simulator the extension method is called.

The same happens with an iPhone 11 iOS 13.5 simulator. On an iPhone 11 iOS 13.3 simulator it works in either case, whether it is launched from Xcode or not.

I'll toss an example project on Github.
Thanks to @steipete on Twitter this seems to be resolved.

Since the category (or rather, its methods) are exposed to the Objective-C runtime with @objc, things can get a little hairy.
Seems like this started to clash with an internal method starting in iOS 13.4, and at that point the behaviour is undefined in the Objective-C runtime. So if you expose extensions/categories to the Objective-C runtime, it's still best practice to prefix them, like you are/were supposed to when writing Objective-C categories.
I think I just ran into this issue in native objective-c code. I am not using swift.

I defined a category on UIButton and created a setTitle method:

Code Block
@interface UIButton (utility)
- (void) setTitle:(NSString*)title;
@end

And my code never gets called. I finally changed the name to "setText".
I really want to know why this is breaking. It makes me worry that using categories is just plain dangerous. Especially if an upgrade of the OS version could potentially break an app that is already distributed because of this type of problem.

After change the name to "setText" I added some debugging code to probe for a selector named "setTitle:" and sure enough, there was already a method, presumably defined by Apple. Here was my code:

Code Block
  if ([button respondsToSelector:@selector(setTitle:)])
 {
  NSLog(@"xyz: has selector setTitle:");
 }
 else
 {
  NSLog(@"xyz: DOES NOT have setTitle:");
 }

And it did log "xyz: has selector setTitle:".

I am thinking that either Apple created a new PRIVATE method "setTitle:" or their own extension to UIButton (or sub-class) named "setTitle:". I lean towards the extension since I believe that a naming conflict with extensions causes ambiguous behavior.

But I would like to know what it going on. In the end, I just wish they would add the official method to UIButton for "setTitle" without the state parameter so we would not have to "patch things up".

UIButton extension method not called
 
 
Q