27

假设我有一个容器控制器,它接受 UIViewControllers 数组并将它们布置好,以便用户可以左右滑动以在它们之间切换。这个容器控制器被包裹在一个导航控制器中,并成为应用程序主窗口的根视图控制器。

每个子控制器向 API 发出请求并加载显示在表格视图中的项目列表。根据显示的项目,可以将按钮添加到导航栏,允许用户对表格视图中的所有项目进行操作。

因为 UINavigationController 只使用其子视图控制器的 UINavigationItems,所以容器控制器需要更新其 UINavigationItem 以与其子视图控制器的 UINavigationItem 同步。

容器控制器似乎需要处理两种情况:

  1. 容器控制器的选定视图控制器发生变化,因此容器控制器的 UINavigationItem 应该更新自身以模仿选定视图控制器的 UINavigationItem。
  2. 子控制器更新其 UINavigationItem 并且容器控制器必须知道更改并更新其 UINavigationItem 以匹配。

我想出的最佳解决方案是:

  1. 在 setSelectedViewController: 方法中查询被选视图控制器的导航项,并更新容器控制器的 UINavigationItem 的 leftBarButtonItems、rightBarButtonItems 和 title 属性与被选视图控制器的 UINavigationItem 相同。
  2. 在 setSelectedViewController 方法 KVO 中,选择视图控制器的 UINavigationItem 的 leftBarButtonItems、rightBarButtonItems 和 title 属性,并且每当这些属性之一更改容器控制器的 UINavigationItem 时。

对于我编写的许多容器控制器来说,这是一个反复出现的问题,我似乎找不到任何记录在案的这些问题的解决方案。

人们针对这个问题找到了哪些解决方案?

4

5 回答 5

6

所以我目前实现的解决方案是在 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

容器控制器.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];
}
于 2013-08-12T17:10:41.890 回答
4

我知道这个问题很老,但我认为我找到了解决这个问题的方法!

a的navigationItem属性在头文件UIViewController的类别/扩展中定义。UINavigationController

该属性定义为:

open var navigationItem: UINavigationItem { get } 

因此,正如我刚刚发现的那样,您可以覆盖容器视图控制器中的属性,在我的例子中:

public override var navigationItem: UINavigationItem {
    return child?.navigationItem ?? super.navigationItem
}

我尝试了这种方法,它对我有用。所有按钮、标题和视图都会随着它们在包含的视图控制器上的变化而显示和更新。

于 2020-02-20T03:39:28.117 回答
2

接受的答案有效,但它违反了 UIViewController 的合同,您的子控制器现在与您的自定义类别紧密耦合,并且必须使用其替代方法才能正常工作......我在使用 RBStoryboardLink 容器时遇到了这个问题,并且也在我自己的自定义选项卡栏控制器,因此将其封装在给定容器类之外很重要,因此我创建了一个具有 mirrorVC 属性的类(通常设置为容器,即监听通知的容器)和一些注册/注销方法(对于navigationItems、toolbarItems、tabBarItems,根据您的需要)。例如在注册/注销 toolbarItems 时:

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

观察动作将处理接收新值并将它们转发到mirrorVC:

- (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 属性不是。我根据这个要点(https://gist.github.com/brentdax/5938102)创建了一个 UIViewController 类别,它为它启用了 KVO 通知,因此它可以在这种情况下工作。注意:上面的要点对于 UINavigationItem 不是必需的,iOS 5~7 会为它发送适当的 KVO 通知,在该类别中,我会收到 UINavigationItems 的双重通知。它对工具栏项完美无缺!

于 2013-10-17T14:18:39.793 回答
0

您是否考虑过不将容器视图控制器包装在 UINavigationController 中,而只是将 UINavigationBar 添加到您的视图中?然后,您可以将子视图控制器的导航项直接推送到该导航栏。本质上,您的容器视图控制器将替换普通的 UIViewController。

于 2013-08-01T19:33:02.583 回答
-2

我知道这是一个旧线程,但我只是遇到了这个问题,并认为其他人也可以。

所以为了以后参考,我做了如下:我向子视图控制器发送了一个块,它只是设置了父 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 相关的逻辑保留在实际上对它感兴趣的视图控制器中。

注意:我应该提到我不是专业人士。这可能只是一种糟糕的方式。但它似乎工作得很好。

于 2015-03-30T22:14:03.133 回答