0

My iOS app crashes when pressing a button found in a custom view for the rightBarButtonItem. A custom view is used because the barButtonItem design requires more than just a button.

Here is the output of the crash:

[UIViewControllerWrapperView buttonPressed:]: unrecognized selector sent to instance 0x7669430]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerWrapperView buttonPressed:]: unrecognized selector sent to instance 0x7669430'

The custom view is defined in a separate view controller's xib, RightBarButtonItemVC, which also contains this linked method:

- (IBAction)buttonPressed:(id)sender {
    NSLog(@"button pressed");
}

The rightBarButtonItemVC is used in viewDidLoad, for all views controllers that need the item:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    RightBarButtonItemVC *rightBarButtonItemVC = [[RightBarButtonItemVC alloc] initWithNibName:@"RightBarButtonItemVC" bundle:nil];
    
    UIBarButtonItem *rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:rightBarButtonItemVC.view]; 
    
    self.navigationItem.rightBarButtonItem = rightBarButtonItem;
}

Notice how I am assigning rightBarButtonItemVC's view as the view for rightBarButtonItem.

Question

  1. Why is an instance of UIViewControllerWrapperView calling my selector instead of my instance of rightBarButtonItemVC?
  2. How can I prevent this from happening and get the button to work? Should I write a category for UIViewControllerWrapperView? If so, where to import the file?
Community
  • 1
  • 1
kraftydevil
  • 5,144
  • 6
  • 43
  • 65
  • I don't think you can "share" a view controller's view like that. Why don't you just create a view in a xib rather than a view controller? – rdelmar Mar 07 '13 at 02:46
  • I thought about that, but if I did that I would have to define the target method in every viewController, right? – kraftydevil Mar 07 '13 at 04:05
  • Why? What does the image you're putting on the button have to do with its target method? – rdelmar Mar 07 '13 at 04:13
  • I'm not sure what you mean as I'm loading a UIBarButtonItem with a custom view, not just applying an image to a button. – kraftydevil Mar 07 '13 at 05:30
  • is there a way I can make this question better? I'm curious about the downvote – kraftydevil Mar 07 '13 at 18:35

2 Answers2

1

UIViewControllerWrapperView is not calling your selector; your button is calling -buttonPressed: on the UIViewControllerWrapperView. Try enabling zombies.

It looks like you're using RightBarButtonItemVC simply as a view loader (I assume you're using ARC, or it would leak). This is expensive, and strange things can happen unless you set rightBarButtonItemVC.view = nil before using the view elsewhere (I forget exactly what). I present a better way to load views from nibs here (I don't know if Interface Builder supports nibs owned by a protocol, which would be ideal).

There are two main reasons your code might be crashing:

  • In the NIB, -buttonPressed: is connected to the wrong thing. I don't think this is likely.
  • -buttonPressed: would get sent to the RightBarButtonItemVC, except the RightBarButtonItemVC is not retained by anything so it gets dealloced. It gets sent to the next object that is allocated at the same address, which happens to be a UIViewControllerWrapperView.

There are two easy fixes:

  • Remove the connection in Interface Builder and add it programmatically with -addTarget:action:forControlEvents:. This requires finding the button in the view hierarchy.
  • Create it programmatically in the first place.

I prefer the latter; in the long run it seems to be far easier to maintain UI in code, and is much easier to localize since you only need to translate a single strings file.

Community
  • 1
  • 1
tc.
  • 33,468
  • 5
  • 78
  • 96
  • TY - I wouldn't say RightBarButtonItemVC is just a view loader - it's supposed to respond to button presses and take the appropriate actions. If I make just a lone xib then I lose the ability to keep all of the button responses in one place. If I understand your method correctly, it implies I need to add targets in viewDidLoad for every new view controller that wants to use the UIBarButtonItem. I was hoping to avoid this if you have a solution that includes that. – kraftydevil Mar 07 '13 at 06:52
  • @kraftydevil Then either something needs to retain the RightBarButtonItemVC or the VCs which want this functionality need to subclass RightBarButtonItemVC (or you can add a category to `UIViewController` but that's a bit icky). Perhaps you could subclass most/all of your VCs from e.g. `BaseViewController` to encapsulate common functionality? – tc. Mar 07 '13 at 19:12
  • yeah RightBarButtonItemVC must not be retained. I tried your addTarget method and it still crashes. I think I'll try a fresh standalone xib combined with a category on UIViewController that can set self.navigationItem.rightBarButtonItem for any UIViewController. At least then it's just one line in each viewDidLoad. – kraftydevil Mar 07 '13 at 20:29
  • @kraftydevil Why bother with a nib to load a single button? – tc. Mar 08 '13 at 02:27
  • tc. while I am setting the rightBarButtonItem, it's not supposed to entirely be a button. The custom view that is loaded contains an image, and then 2 buttons. – kraftydevil Mar 08 '13 at 05:27
  • @kraftydevil That's easy enough to do in a few lines of code. – tc. Mar 08 '13 at 13:57
0

Direct Answers:

  1. As suggested by @tc.'s answer, there is a disconnect somewhere between defining the view in a xib and using a View Controller (RightBarButtonItemVC) to define a custom view on a UIBarButtonItem, which is evident in the fact that UIViewControllerWrapperView receives the buttonPressed call instead of RightBarButtonItemVC. It looks like something is not being retained, although I'm not sure what.
  2. What follows is the specific working solution that I implemented. I did make a category, but not for UIViewControllerWrapperView as previously mentioned.

Specific Solution:

First create BarButtonItemLoader, an Objective-C category on UIViewController:

@interface UIViewController (BarButtonItemLoader)

In UIViewController+BarButtonItemLoader.h, define this method:

- (UIBarButtonItem *) rightBarButtonItem;

Since you can't keep track of state in a category, define a UIBarButtonItem in AppDelegate.h:

@property (strong, nonatomic) UIBarButtonItem *rightBarButtonItem;

Next, start implementing the category's rightBarButtonItem method by lazy loading the rightBarButtonItem from the AppDelegate (don't forget to #import "AppDelegate.h"). This ensures only one rightBarButtonItem will be created and retained in the AppDelegate:

- (UIBarButtonItem *) rightBarButtonItem {

    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];

    if(!appDelegate.rightBarButtonItem) {
        //create a rightBarButtonItem (see below)
    }
    return appDelegate.rightBarButtonItem;
}

Start assembling a UIView/UIBarButtonItem that will be set to the rightBarButtonItem. Transfer each element/configuration from the old Interface Builder / xib implementation. Most importantly take note of the frame information in the Size inspector so you can programmatically position your subviews just how you had them manually positioned in the .xib file.

- (UIBarButtonItem *) rightBarButtonItem {

    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];

    if(!appDelegate.rightBarButtonItem) {
        UIView *rightBarView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 264, 44)];
        UIBarButtonItem *rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:rightBarView];
        UIImageView *textHeader = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"textHeader.png"]];
        textHeader.frame = CGRectMake(2, 14, 114, 20);
        [rightBarView addSubview:textHeader];

        UIButton *button1 = [[UIButton alloc] initWithFrame:CGRectMake(100, 2, 70, 44)];
        [button1 setImage:[UIImage imageNamed:@"button1.png"] forState:UIControlStateNormal];
        [button1 setImage:[UIImage imageNamed:@"button1Highlighted.png"] forState:UIControlStateHighlighted];
        [button1 addTarget:self action:@selector(button1Pressed) forControlEvents:UIControlEventTouchUpInside];
        [rightBarView addSubview:button1];

        UIButton *button2 = [[UIButton alloc] initWithFrame:CGRectMake(194, 2, 70, 44)];
        [button2 setImage:[UIImage imageNamed:@"button2.png"] forState:UIControlStateNormal];
        [button2 setImage:[UIImage imageNamed:@"button2Highlighted.png"] forState:UIControlStateHighlighted];
        [button2 addTarget:self action:@selector(button2Pressed) forControlEvents:UIControlEventTouchUpInside];
        [rightBarView addSubview:button2];

        appDelegate.rightBarButtonItem = rightBarButtonItem;
    }
    return appDelegate.rightBarButtonItem;
}

Finally, implement the buttonXPressed methods in UIViewController+BarButtonItemLoader.m to your purpose:

- (void) button1Pressed {
      NSLog(@"button1 Pressed");
}

- (void) button2Pressed {
      NSLog(@"button2 Pressed");
}

...

Use the category by adding this code to any UIViewController or subclass thereof:

#import "UIViewController+BarButtonItemLoader.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.rightBarButtonItem = [self rightBarButtonItem];
}

Summary

This approach allows you to add a UIBarButtonItem on-the-fly to any UIViewController. The drawback is that you must add the above code to all UIViewControllers you create.

Another Option

If you want to further encapsulate the addition of UIBarButtonItems (or anything else), avoiding the need to add code in each View Controller, you should create a BaseViewController from which you then subclass all of your other View Controllers. From there you can consider other items that you want to include in all your View Controllers. Choosing the Category or Subclass route then becomes a question of granularity.

kraftydevil
  • 5,144
  • 6
  • 43
  • 65