Using UIAppearance with a subclass of UIBarButtonItem is causing unrecognized selector being sent to UINavigationButton
Asked Answered
C

2

6

TL&DR;

Setting a custom property of a subclass of UIBarButtonItem using UIAppearance proxy is causing the "unrecognized selector" exception, beause the setter is probably being forwarded by UIAppearance to UINavigationButton, not the bar button itself.


SDK overview

I am using iOS 7 Beta 5 SDK with Xcode 5 DP5. But please, don't tell me that it is under NDA, because I do not discuss any new features or new classes in this question. I inform you of my SDK, because it can be found out that it's just a bug in the beta software.

What I did

I subclassed a UIBarButtonItem and created a custom property in the header file:

@property (nonatomic, strong) NSString *mySubclassedProperty UI_APPEARANCE_SELECTOR;

My setter and getter look like this:

- (void)setMySubclassedProperty:(NSString *)mySubclassedProperty {
    _mySubclassedProperty = mySubclassedProperty;
    NSLog(@"%p %s %@", self, __PRETTY_FUNCTION__, mySubclassedProperty);
}

Nothing special, huh? But it doesn't work with UIAppearance at all. When I try to set the default appearance in my application's delegate, it gives me no error, no warning, whatsoever.

[[AKBarButtonItem appearance] setMySubclassedProperty:@"GLOBALLY ASSIGNED"];

The crash

It seems to be working like a charm, except of the fact that it crashes when I try to set an instance of AKBarButtonItem to self.navigationItem.rightBarButtonItem:

self.navigationItem.rightBarButtonItem = [[AKBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:nil action:nil];

The backtrace looks like this:

2013-08-13 14:30:08.551 UIBarButtonItem Subclass Demo[1512:a0b] -[UINavigationButton setMySubclassedProperty:]: unrecognized selector sent to instance 0x8c42710
2013-08-13 14:30:08.556 UIBarButtonItem Subclass Demo[1512:a0b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UINavigationButton setMySubclassedProperty:]: unrecognized selector sent to instance 0x8c42710'
*** First throw call stack:
(
    0   CoreFoundation                      0x0173b6f4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x014bb8b6 objc_exception_throw + 44
    2   CoreFoundation                      0x017d8983 -[NSObject(NSObject) doesNotRecognizeSelector:] + 275
    3   CoreFoundation                      0x0172ba1b ___forwarding___ + 1019
    4   CoreFoundation                      0x0172b5fe _CF_forwarding_prep_0 + 14
    5   CoreFoundation                      0x0172fe2d __invoking___ + 29
    6   CoreFoundation                      0x0172fd3a -[NSInvocation invoke] + 362
    7   CoreFoundation                      0x0172feba -[NSInvocation invokeWithTarget:] + 74
    8   UIKit                               0x00763255 workaround10030904InvokeWithTarget + 824
    9   UIKit                               0x0075dea8 +[_UIAppearance _applyInvocationsTo:window:matchingSelector:] + 4497
    10  UIKit                               0x0075e299 +[_UIAppearance _applyInvocationsTo:window:] + 55
    11  UIKit                               0x0029ddcb -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 183
    12  libobjc.A.dylib                     0x014cd81f -[NSObject performSelector:withObject:] + 70
    13  QuartzCore                          0x03ac172a -[CALayer layoutSublayers] + 148
    14  QuartzCore                          0x03ab5514 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380
    15  QuartzCore                          0x03ac3b55 -[CALayer(CALayerPrivate) layoutBelowIfNeeded] + 43
    16  UIKit                               0x00290886 -[UIView(Hierarchy) layoutBelowIfNeeded] + 595
    17  UIKit                               0x0029062d -[UIView(Hierarchy) layoutIfNeeded] + 74
    18  UIKit                               0x0050841e -[UIBarButtonItem(UIStatic) _leftRightImagePaddingForEdgeMarginInNavBarIsMini:] + 574
    19  UIKit                               0x002ea325 -[UINavigationBar _getTitleViewFrame:leftViewFrames:rightViewFrames:forItemAtIndex:returnedIdealWidthOfTextContent:availableLayoutWidthForTextContent:idealBackButtonWidth:] + 3522
    20  UIKit                               0x002ef2da -[UINavigationBar _getTitleViewFrame:leftViewFrames:rightViewFrames:forItemAtIndex:] + 591
    21  UIKit                               0x002ef59a -[UINavigationBar _getTitleViewFrame:leftViewFrames:rightViewFrames:] + 151
    22  UIKit                               0x002dc515 -[UINavigationBar _setLeftViews:rightViews:] + 1894
    23  UIKit                               0x002ca6c5 -[UINavigationItem updateNavigationBarButtonsAnimated:] + 188
    24  UIKit                               0x002cab63 -[UINavigationItem setObject:forLeftRightKeyPath:animated:] + 547
    25  UIKit                               0x002cb1ae -[UINavigationItem setRightBarButtonItem:animated:] + 171
    26  UIKit                               0x002cb0fe -[UINavigationItem setRightBarButtonItem:] + 48
    27  UIBarButtonItem Subclass Demo       0x0000665e -[AKViewController viewDidLoad] + 222
    28  UIKit                               0x00345c18 -[UIViewController loadViewIfRequired] + 696
    29  UIKit                               0x00345eb4 -[UIViewController view] + 35
    30  UIKit                               0x00370369 -[UINavigationController rotatingSnapshotViewForWindow:] + 52
    31  UIKit                               0x00698e10 -[UIClientRotationContext initWithClient:toOrientation:duration:andWindow:] + 420
    32  UIKit                               0x00276ff2 -[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:isRotating:] + 1495
    33  UIKit                               0x00276a16 -[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:] + 82
    34  UIKit                               0x002768e8 -[UIWindow _setRotatableViewOrientation:updateStatusBar:duration:force:] + 117
    35  UIKit                               0x00276970 -[UIWindow _setRotatableViewOrientation:duration:force:] + 67
    36  UIKit                               0x00275a0a __57-[UIWindow _updateToInterfaceOrientation:duration:force:]_block_invoke + 120
    37  UIKit                               0x0027596c -[UIWindow _updateToInterfaceOrientation:duration:force:] + 400
    38  UIKit                               0x002766c3 -[UIWindow setAutorotates:forceUpdateInterfaceOrientation:] + 870
    39  UIKit                               0x00279c7e -[UIWindow setDelegate:] + 449
    40  UIKit                               0x0034a037 -[UIViewController _tryBecomeRootViewControllerInWindow:] + 180
    41  UIKit                               0x0026f91c -[UIWindow addRootViewControllerViewIfPossible] + 609
    42  UIKit                               0x00270146 -[UIWindow setRootViewController:] + 960
    43  UIBarButtonItem Subclass Demo       0x00006a07 -[AKAppDelegate application:didFinishLaunchingWithOptions:] + 823
    44  UIKit                               0x0022d525 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 309
    45  UIKit                               0x0022dd65 -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1536
    46  UIKit                               0x00232578 -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 824
    47  UIKit                               0x0024657c -[UIApplication handleEvent:withNewEvent:] + 3447
    48  UIKit                               0x00246ae9 -[UIApplication sendEvent:] + 85
    49  UIKit                               0x002341f5 _UIApplicationHandleEvent + 736
    50  GraphicsServices                    0x0365a33b _PurpleEventCallback + 776
    51  GraphicsServices                    0x03659e46 PurpleEventCallback + 46
    52  CoreFoundation                      0x016b6e95 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53
    53  CoreFoundation                      0x016b6bcb __CFRunLoopDoSource1 + 523
    54  CoreFoundation                      0x016e18ac __CFRunLoopRun + 2156
    55  CoreFoundation                      0x016e0bf3 CFRunLoopRunSpecific + 467
    56  CoreFoundation                      0x016e0a0b CFRunLoopRunInMode + 123
    57  UIKit                               0x00231cad -[UIApplication _run] + 840
    58  UIKit                               0x00233f0b UIApplicationMain + 1225
    59  UIBarButtonItem Subclass Demo       0x00006e9d main + 141
    60  libdyld.dylib                       0x01d77725 start + 0
)
libc++abi.dylib: terminating with uncaught exception of type NSException

After some examination I strangly realized that the setMySubclassedProperty: message is sent to UINavigationButton (which is a private subclass of UIButton used in navigation bars, toolbars and search bars). I set the button in frame 26 and the UIAppearance magic is executed in frame 10.

In iOS 6, the issue exists but a different message is sent to UINavigationButton:

2013-08-13 14:35:58.316 UIBarButtonItem Subclass Demo[1591:c07] -[UINavigationButton _UIAppearance_setMySubclassedProperty:]: unrecognized selector sent to instance 0x810d600
2013-08-13 14:35:58.359 UIBarButtonItem Subclass Demo[1591:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UINavigationButton _UIAppearance_setMySubclassedProperty:]: unrecognized selector sent to instance 0x810d600'
*** First throw call stack:
(0x12b2012 0x10d7e7e 0x133d4bd 0x12a1bbc 0x12a194e 0x12a61bd 0x12a60d6 0x12a624a 0x68b9e8 0x68769c 0x687a95 0x291884 0x3088ca 0x28cf96 0x2934a4 0x28b89d 0x2cc646 0x2bc076 0x2bc517 0x2bcb66 0x2bcab6 0x665e 0x3261c7 0x326232 0x3264da 0x33d8e5 0x33d9cb 0x33dc76 0x33dd71 0x33e89b 0x33e9b9 0x33ea45 0x44420b 0x2952dd 0x10eb6b0 0x289dfc0 0x289233c 0x289deaf 0x3342bd 0x27cb56 0x27b66f 0x27b589 0x27a7e4 0x27a61e 0x27b3d9 0x27e2d2 0x32899c 0x275574 0x275cc1 0x6a07 0x242157 0x242747 0x24394b 0x254cb5 0x255beb 0x247698 0x24b8df9 0x24b8ad0 0x1227bf5 0x1227962 0x1258bb6 0x1257f44 0x1257e1b 0x24317a 0x244ffc 0x6e9d 0x1ab0725)
libc++abi.dylib: terminate called throwing an exception

Deduction

My first thought was: "UIAppearance forwards the setter to AKBarButtonItem and then it crashes". But does it? In an article by Peter Steinberger I found out that Apple is using a special subclass of _UIAppearance for bar items which is called _UIBarItemAppearance. I wanted to confirm that and set a symbolic breakpoint on -[_UIBarItemAppearance forwardInvocation:] and lldb successfully halted just before the exception occurred.

Now it seems to me that Apple is doing some dirty logic in -[_UIBarItemAppearance forwardInvocation:] to realize what selector should be sent to what instance, like:

  • setBackgroundImage:forState: should be sent to UINavigationButton
  • setBackButtonBackgroundImage:forState:barMetrics: to UIBarButtonItem
  • ...

And their code looks like this:

if (selector == @selector(setBackButtonBackgroundImage:forState:barMetrics:)) {
    // forward to UIBarButtonItem
} else if (selector == @selector(setBackButtonBackgroundVerticalPositionAdjustment:forBarMetrics:)) {
    // forward to UIBarButtonItem
} else if (...) {
    // more forwarding to UIBarButtonItem
} else {
    // forward to UINavigationButton
}

Then, according to my deduction, the last else statement is causing setNiceBarButtonItemAttributes: selector being sent to UINavigationButton, not UIBarButtonItem. If it's true, we're doomed.


Sample code

You can download a sample project here: http://cl.ly/241I2A3U0a0y.


Help

Am I just doing something wrong, or is it a bug? Or maybe Apple did it on purpose? Any help would be appreciated.

Cleotildeclepe answered 13/8, 2013 at 8:39 Comment(0)
C
0

OK, I ended up using a customView. Seems that it's the only way..

Cleotildeclepe answered 1/9, 2013 at 22:41 Comment(0)
K
0

The problem is UIBarButtonItem is not a subclass of UIView, it is a "controller" class which creates private UIView subclasses. For bar button items, there is a private appearance proxy which is used, instead of the usual one (some information here).

May I suggest going a different route? Do you really need to subclass bar button items for your customization? Could you not create a subclass of UINavigationBar and use that as your trigger for changes with appearanceWhenContainedIn:?

Karankaras answered 13/8, 2013 at 10:1 Comment(6)
Yep, I read that article (I even linked it in the question). But I'm afraid it has to be done at UIBarButtonItem-level. The only option that comes to my mind is to create a UIBarButtonItem with a custom view. In that case, no UINavigationButton will be created.Cleotildeclepe
From experience, custom view buttons are a bitch to work with, and never look native. You are already working with 7 SDK, it's a big pain in the ass to have then draw correctly in various scenarios (e.g. landscape vs portrait on Phone) and across different OS versions.Chymotrypsin
Yep, I agree. Nevertheless, I filled a bug report under rdar://14722948. I hope they will fix the issue or at least respond before the final release of iOS 7.Cleotildeclepe
I added a sample project that I submitted to Apple in the question's body.Cleotildeclepe
Have you tried your code with an older SDK? Is the bug still present? If so, even if they fix, are you willing to let go of the 6.X crowd? Within a year, 6.X will be irrelevant just as 5.X is these days, but initially at least, it could be problem.Chymotrypsin
Yep, the issue is present in iOS 6.0 and iOS 6.1. I updated the question. I am developing something that will still take at least 9 months to finish, so I am not worrying about that as for now. In addition, the problem in this question is not connected with my application - I need it for an open-source library that I am creating.Cleotildeclepe
C
0

OK, I ended up using a customView. Seems that it's the only way..

Cleotildeclepe answered 1/9, 2013 at 22:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.