处理容器控制器中的子视图控制器的UINavigationItem更改的最佳实践?

问题描述 投票:23回答:5

假设我有一个容器控制器,该容器控制器接受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。

这是我编写的许多容器控制器的重复出现的问题,我似乎找不到针对这些问题的任何书面解决方案。

人们对此问题有什么解决方案?

ios cocoa-touch uinavigationcontroller uinavigationitem
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

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属性的类(通常设置为容器,一个将监听通知的容器)和一些注册/注销方法(根据需要,用于navigationItems,toolbarItems,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];
}

观察动作将处理接收新值并将其转发到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的双重通知。它对于toolbarItems而言完美无缺!


0
投票

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


0
投票

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

navigationItemUIViewController属性在UINavigationController头文件的类别/扩展名中定义。

此属性定义为:

open var navigationItem: UINavigationItem { get } 

所以,正如我刚刚发现的那样,在我的情况下,您可以在容器视图控制器中覆盖属性:

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

我尝试了这种方法,现在对我有用。在包含的视图控制器上更改时,所有按钮,标题和视图都会显示并更新。


-1
投票

我知道这是一个老话题,但我刚遇到这个问题,并认为其他人也可能会。

因此,为了将来参考,我这样做如下:我向子视图控制器发送了一个块,该子视图控制器仅设置了父视图的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有关的逻辑,而该控制器实际上是其中的[[interested

注意:我应该说我不是专业人士。这可能只是一种糟糕的方法。但这似乎很好用。
© www.soinside.com 2019 - 2024. All rights reserved.