Рекомендации по обработке изменений в UINavigationItem контроллеров дочерних представлений в контроллере контейнера?
Предположим, что у меня есть контроллер контейнера, который принимает массив UIViewControllers и выдает их, чтобы пользователь мог прокручивать влево и вправо на переход между ними. Этот контроллер контейнера обернут внутри контроллера навигации и стал контроллером корневого представления главного окна приложения.
Каждый дочерний контроллер выполняет запрос к API и загружает список элементов, отображаемых в виде таблицы. На основе отображаемых элементов на навигационной панели может быть добавлена кнопка, которая позволяет пользователю действовать со всеми элементами в представлении таблицы.
Поскольку UINavigationController использует только UINavigationItems своих дочерних контроллеров представления, контроллер контейнера должен обновить свой UINavigationItem, чтобы синхронизировать с UINavigationItem своих дочерних элементов.
Кажется, существуют два сценария, которые должен обрабатывать контроллер контейнера:
- Изменяется выбранный контроллер представления контроллера контейнера, поэтому UINavigationItem контроллера контейнера должен обновить себя, чтобы имитировать UINavigationItem выбранного контроллера представления.
- Детский контроллер обновляет свой UINavigationItem, и контроллер контейнера должен быть ознакомлен с этим изменением и обновить его UINavigationItem для соответствия.
Лучшие решения, которые я придумал:
- В методе setSelectedViewController: запросите элемент навигации выбранного контроллера представления и обновите свойства leftBarButtonItems, rightBarButtonItems и свойства заголовка контроллера UINavigationItem контейнера, чтобы он был таким же, как выбранный UINavigationItem контроллера.
- В методе setSelectedViewController KVO на свойства leftBarButtonItems, rightBarButtonItems и свойство title выбранного контроллера UINavigationItem для просмотра и всякий раз, когда одно из этих свойств изменяет контейнерный контроллер UINavigationItem.
Это повторяющаяся проблема со многими контроллерами контейнеров, которые я написал, и я не могу найти каких-либо документированных решений этих проблем.
Какие решения люди нашли для этой проблемы?
Ответы
Ответ 1
Таким образом, решение, которое я в настоящее время реализует, - это создать категорию на UIViewController с помощью методов, которые позволяют вам устанавливать кнопки правого штриха элемента навигации контроллера, а затем этот контроллер отправляет уведомление, позволяющее любому, кто заботится, знать, что правая полоса элементы кнопки были изменены.
В моем контроллере контейнера я слушаю это уведомление от текущего выбранного контроллера представления и соответствующим образом обновляю элемент навигации контроллера контейнера.
В моем сценарии контейнерный контроллер переопределяет метод в категории, чтобы он мог сохранить локальную копию элементов правой кнопки панели, которые ему были назначены, и, если какие-либо уведомления подняты, он объединяет элементы правой кнопки панели с ее а затем отправляет уведомление только в том случае, если оно также находится внутри контроллера контейнера.
Вот код, который я использую.
UIViewController + ContainerNavigationItem.h
#import <UIKit/UIKit.h>
extern NSString *const UIViewControllerRightBarButtonItemsChangedNotification;
@interface UIViewController (ContainerNavigationItem)
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems;
- (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem;
@end
UIViewController + ContainerNavigationItem.m
#import "UIViewController+ContainerNavigationItem.h"
NSString *const UIViewControllerRightBarButtonItemsChangedNotification = @"UIViewControllerRightBarButtonItemsChangedNotification";
@implementation UIViewController (ContainerNavigationItem)
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
[[self navigationItem] setRightBarButtonItems:rightBarButtonItems];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:UIViewControllerRightBarButtonItemsChangedNotification object:self];
}
- (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem
{
if(rightBarButtonItem != nil)
[self setRightBarButtonItems:@[ rightBarButtonItem ]];
else
[self setRightBarButtonItems:nil];
}
@end
ContainerController.m
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
_rightBarButtonItems = rightBarButtonItems;
[super setRightBarButtonItems:_rightBarButtonItems];
}
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
if(_selectedViewController != selectedViewController)
{
if(_selectedViewController != nil)
{
// Stop listening for right bar button item changed notification on the view controller.
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter removeObserver:self name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController];
}
_selectedViewController = selectedViewController;
if(_selectedViewController != nil)
{
// Listen for right bar button item changed notification on the view controller.
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(_childRightBarButtonItemsChanged) name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController];
}
}
}
- (void)_childRightBarButtonItemsChanged
{
NSArray *childRightBarButtonItems = [[_selectedViewController navigationItem] rightBarButtonItems];
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray:_rightBarButtonItems];
[rightBarButtonItems addObjectsFromArray:childRightBarButtonItems];
[super setRightBarButtonItems:rightBarButtonItems];
}
Ответ 2
Принятый ответ работает, но он нарушает контракт на UIViewController, ваши дочерние контроллеры теперь тесно связаны с вашей настраиваемой категорией и должны использовать свои альтернативные методы для правильной работы...
У меня была проблема с контейнером RBStoryboardLink, а также с собственным контроллером панели вкладок, поэтому было важно, чтобы он был инкапсулирован за пределы заданного класса контейнера, поэтому я создал класс, обладающий свойством mirrorVC (обычно устанавливается как контейнер, тот, кто будет прослушивать уведомления) и несколько методов регистрации/отмены регистрации (для элементов навигации, элементов панели инструментов, tabBarItems, так как ваши потребности подходят).
Например, при регистрации/незарегистрировании для элементов панели инструментов:
static void *myContext = &myContext;
-(void)registerForToolbarItems:(UIViewController*)viewController {
[viewController addObserver:self forKeyPath:@"toolbarItems" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:myContext];
}
-(void)unregisterForToolbarItems:(UIViewController*)viewController {
[viewController removeObserver:self forKeyPath:@"toolbarItems" context:myContext];
}
Действие наблюдения будет обрабатывать получение новых значений и пересылку их в зеркало:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context == myContext) {
id newKey = [change objectForKey:NSKeyValueChangeNewKey];
id oldKey = [change objectForKey:NSKeyValueChangeOldKey];
//no need to mirror if the value is the same
if ([newKey isEqual:oldKey]) return;
//nil values comes packaged in NSNull
if (newKey == [NSNull null]) newKey = nil;
//handle each of the possibly registered mirrored properties...
if ([keyPath isEqualToString:@"navigationItem.leftBarButtonItem"]) {
self.mirrorVC.navigationItem.leftBarButtonItem = newKey;
}
//...
//as many more properties as you need forwarded...
else if ([keyPath isEqualToString:@"toolbarItems"]) {
[self.mirrorVC setToolbarItems:newKey animated:YES];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Затем в вашем контейнере, в нужные моменты, вы регистрируете и отменяете регистрацию
[_selectedViewController unregister...]
_selectedViewController = selectedViewController;
[_selectedViewController register...]
Вы должны знать о потенциальной ловушке: не все желательные свойства соответствуют требованиям KVO, а те, которые это делают, не документированы, - поэтому они могут перестать быть или ошибочно вести себя в любое время.
Свойство toolbarItems, например, нет. Я создал категорию UIViewController, основанную на этом gist (https://gist.github.com/brentdax/5938102), который позволяет KVO-уведомлениям, чтобы он работал в этом сценарии. Примечание: для UINavigationItem не было необходимости в том, что для UINavigationItem, iOS 5 ~ 7 отправляет для него соответствующие уведомления KVO, причем эта категория я получаю двойные уведомления для UINavigationItems. Он работал безупречно для toolbarItems!
Ответ 3
Считаете ли вы, что НЕ упаковываете контроллер контейнера в UINavigationController и просто добавляете UINavigationBar к вашему представлению? Затем вы можете направить навигационные элементы контроллера вашего ребенка прямо на эту панель навигации. По сути ваш контроллер контейнера заменит обычный UIViewController.
Ответ 4
Я знаю, что это старый поток, но я просто столкнулся с этой проблемой и подумал, что кто-то еще может это сделать.
Итак, для справок в будущем я сделал это следующим образом: я отправил блок в контроллер дочернего представления, который просто устанавливает правильную кнопку родительского UINavigationItem. Затем я создал UIBarButtonItem как обычно в контроллере дочерних представлений, вызывая некоторый метод в том же контроллере.
Итак, в ChildViewController.h:
// Declare block property
@property (nonatomic, copy) void (^setRightBarButtonBlock)(UIBarButtonItem*);
И в ChildViewController.m:
self.myBarButton = [[UIBarButtonItem alloc]
initWithTitle:@"My Title"
style:UIBarButtonItemStylePlain
target:self
action:@selector(didPressMyBarButton:)];
...
// Show bar button in navigation bar
// As normal, just call it with 'nil' to hide the button
if (self.setRightBarButtonBlock) {
self.setRightBarButtonBlock(self.myBarButton);
}
...
- (void)didPressMyBarButton:(UIBarButtonItem *)sender {
// Do something here
}
И, наконец, в ParentViewController.m
// Initialise child view controller
ChildViewController *child = [[ChildViewController alloc] init];
// Give it block for changing bar button item
__weak typeof(self) weakSelf = self;
child.setRightBarButtonBlock = ^void(UIBarButtonItem *barButtonItem) {
[weakSelf.navigationItem setRightBarButtonItem:barButtonItem animated:YES];
};
// Finish the parent-child VC dance
Что это. Это хорошо для меня, потому что он сохраняет логику, относящуюся к UIBarButtonItem, в контроллере представления, который действительно заинтересован в этом.
Примечание. Я должен упомянуть, что я не профессионал. Это может быть просто ужасным способом сделать это. Но, похоже, все работает нормально.