Почему что-то еще вызывает метод, подключенный к UIBarButtonItem через xib Interface Builder?

Мое приложение iOS аварийно завершает работу при нажатии кнопки, найденной в пользовательском представлении для rightBarButtonItem. Пользовательское представление используется, потому что для дизайна barButtonItem требуется больше, чем просто кнопка.

Вот результат сбоя:

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

Пользовательское представление определено в xib отдельного контроллера представления, RightBarButtonItemVC, который также содержит этот связанный метод:

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

rightBarButtonItemVC используется в viewDidLoad для всех контроллеров представлений, которым нужен этот элемент:

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

Обратите внимание, как я назначаю представление rightBarButtonItemVC в качестве представления для rightBarButtonItem.

Вопрос

  1. Почему экземпляр UIViewControllerWrapperView вызывает мой селектор вместо моего экземпляра rightBarButtonItemVC?
  2. Как я могу предотвратить это и заставить кнопку работать? Должен ли я написать категорию для UIViewControllerWrapperView? Если да, то куда импортировать файл?

person kraftydevil    schedule 07.03.2013    source источник
comment
Я не думаю, что вы можете поделиться таким представлением контроллера представления. Почему бы вам просто не создать представление в xib, а не в контроллере представления?   -  person rdelmar    schedule 07.03.2013
comment
Я думал об этом, но если бы я это сделал, мне пришлось бы определять целевой метод в каждом viewController, верно?   -  person kraftydevil    schedule 07.03.2013
comment
Почему? Какое отношение изображение, которое вы помещаете на кнопку, имеет к ее целевому методу?   -  person rdelmar    schedule 07.03.2013
comment
Я не уверен, что вы имеете в виду, поскольку я загружаю UIBarButtonItem с настраиваемым представлением, а не просто применяю изображение к кнопке.   -  person kraftydevil    schedule 07.03.2013
comment
есть ли способ сделать этот вопрос лучше? меня интересует минус   -  person kraftydevil    schedule 07.03.2013


Ответы (2)


UIViewControllerWrapperView не вызывает ваш селектор; ваша кнопка вызывает -buttonPressed: на UIViewControllerWrapperView. Попробуйте включить зомби.

Похоже, вы используете RightBarButtonItemVC просто как загрузчик представления (я предполагаю, что вы используете ARC, иначе произойдет утечка). Это дорого, и могут произойти странные вещи, если вы не установите rightBarButtonItemVC.view = nil перед использованием представления в другом месте (я точно не помню, что именно). Я представляю лучший способ загрузки представлений из перьев здесь (я не знаю, поддерживает ли Interface Builder перья, принадлежащие протоколу, что было бы идеально).

Есть две основные причины, по которым ваш код может дать сбой:

  • В NIB -buttonPressed: связано не с тем. Я не думаю, что это вероятно.
  • -buttonPressed: будет отправлено в RightBarButtonItemVC, за исключением того, что RightBarButtonItemVC ничем не удерживается, поэтому он освобождается. Он отправляется следующему объекту, выделенному по тому же адресу, который оказывается UIViewControllerWrapperView.

Есть два простых решения:

  • Удалите соединение в Interface Builder и добавьте его программно с помощью -addTarget:action:forControlEvents:. Для этого необходимо найти кнопку в иерархии представлений.
  • Создайте его программно в первую очередь.

Я предпочитаю последнее; в долгосрочной перспективе кажется, что гораздо проще поддерживать пользовательский интерфейс в коде, и его намного проще локализовать, поскольку вам нужно всего лишь перевести один строковый файл.

person tc.    schedule 07.03.2013
comment
TY — я бы не сказал, что RightBarButtonItemVC — это просто загрузчик представлений — он должен реагировать на нажатия кнопок и выполнять соответствующие действия. Если я сделаю только один xib, я потеряю возможность хранить все ответы кнопок в одном месте. Если я правильно понимаю ваш метод, это означает, что мне нужно добавить цели в viewDidLoad для каждого нового контроллера представления, который хочет использовать UIBarButtonItem. Я надеялся избежать этого, если у вас есть решение, которое включает это. - person kraftydevil; 07.03.2013
comment
@kraftydevil Тогда либо что-то должно сохранить RightBarButtonItemVC, либо венчурные капиталисты, которым нужна эта функциональность, должны создать подкласс RightBarButtonItemVC (или вы можете добавить категорию в UIViewController, но это немного неприятно). Возможно, вы могли бы подклассифицировать большинство/все ваши венчурные капиталисты, например, из BaseViewController для инкапсуляции общей функциональности? - person tc.; 07.03.2013
comment
да, RightBarButtonItemVC не должен сохраняться. Я попробовал ваш метод addTarget, и он все еще падает. Я думаю, что попробую новый автономный xib в сочетании с категорией в UIViewController, который может установить self.navigationItem.rightBarButtonItem для любого UIViewController. По крайней мере, тогда это всего одна строка в каждом viewDidLoad. - person kraftydevil; 08.03.2013
comment
@kraftydevil Зачем возиться с пером, чтобы загрузить одну кнопку? - person tc.; 08.03.2013
comment
тк. пока я устанавливаю rightBarButtonItem, это не должно быть полностью кнопкой. Загружаемый пользовательский вид содержит изображение, а затем 2 кнопки. - person kraftydevil; 08.03.2013
comment
@kraftydevil Это достаточно просто сделать с помощью нескольких строк кода. - person tc.; 08.03.2013

Прямые ответы:

  1. Как указано в ответе @tc., где-то существует разрыв между определением представления в xib и использованием контроллера представления (RightBarButtonItemVC) для определения пользовательского представления в UIBarButtonItem, что проявляется в том факте, что UIViewControllerWrapperView получает buttonPressed вызовите вместо RightBarButtonItemVC. Похоже, что-то не сохраняется, хотя я не уверен, что.
  2. Далее следует конкретное рабочее решение, которое я реализовал. Я создал категорию, но не для UIViewControllerWrapperView, как упоминалось ранее.

Конкретное решение:

Сначала создайте BarButtonItemLoader, категорию Objective-C в UIViewController:

@interface UIViewController (BarButtonItemLoader)

В UIViewController+BarButtonItemLoader.h определите этот метод:

- (UIBarButtonItem *) rightBarButtonItem;

Поскольку вы не можете отслеживать состояние в категории, определите UIBarButtonItem в AppDelegate.h:

@property (strong, nonatomic) UIBarButtonItem *rightBarButtonItem;

Затем начните реализацию метода rightBarButtonItem категории путем ленивой загрузки rightBarButtonItem из AppDelegate (не забудьте #import "AppDelegate.h"). Это гарантирует, что только один rightBarButtonItem будет создан и сохранен в AppDelegate:

- (UIBarButtonItem *) rightBarButtonItem {

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

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

Начните сборку UIView/UIBarButtonItem, для которого будет установлено значение rightBarButtonItem. Перенесите каждый элемент/конфигурацию из старой реализации Interface Builder/xib. Самое главное, обратите внимание на информацию о кадре в инспекторе размера, чтобы вы могли программно расположить свои подпредставления точно так же, как вы разместили их вручную в файле .xib.

- (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;
}

Наконец, реализуйте методы buttonXPressed в UIViewController+BarButtonItemLoader.m для своих целей:

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

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

...

Используйте категорию, добавив этот код в любой UIViewController или его подкласс:

#import "UIViewController+BarButtonItemLoader.h"

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

Резюме

Этот подход позволяет вам добавлять UIBarButtonItem на лету в любой UIViewController. Недостатком является то, что вы должны добавить приведенный выше код ко всем создаваемым вами UIViewController.

Другой вариант

Если вы хотите дополнительно инкапсулировать добавление UIBarButtonItems (или чего-то еще), избегая необходимости добавлять код в каждый контроллер представления, вам следует создать BaseViewController, из которого вы затем подклассируете все остальные контроллеры представления. Оттуда вы можете рассмотреть другие элементы, которые вы хотите включить во все ваши контроллеры представления. Выбор маршрута категории или подкласса становится вопросом детализации.

person kraftydevil    schedule 14.03.2013
comment
Я призываю всех также проголосовать за ответ @tc., так как это привело меня к моему решению. - person kraftydevil; 14.03.2013