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

Posted

技术标签:

【中文标题】处理容器控制器中子视图控制器的 UINavigationItem 更改的最佳实践?【英文标题】:Best practices for handling changes to the UINavigationItem of child view controllers in a container controller? 【发布时间】:2013-08-01 19:21:49 【问题描述】:

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

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

由于 UINavigationController 仅使用其子视图控制器的 UINavigationItems,因此容器控制器需要更新其 UINavigationItem 以与其子视图控制器的 UINavigationItem 同步。

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

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

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

    在setSelectedViewController:方法中查询选中视图控制器的导航项,更新容器控制器UINavigationItem的leftBarButtonItems、rightBarButtonItems和title属性与选中视图控制器的UINavigationItem一致。 在 setSelectedViewController 方法 KVO 中,选择视图控制器的 UINavigationItem 的 leftBarButtonItems、rightBarButtonItems 和 title 属性,以及每当这些属性之一更改容器控制器的 UINavigationItem 时。

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

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

【问题讨论】:

容器视图控制器能否覆盖 - (UINavigationItem *) navigationItem 和只是 return selectedViewController.navigationItem; 不能,因为当 selectedViewController 发生变化时,navigationItem 将不会被再次调用,因此 UINavigationController 仍将使用先前选择的视图控制器的 UINavigationItem。 这样的事情可能会奏效,但我没有尝试过:+ (NSSet *)keyPathsForValuesAffectingNavigationItem return [NSSet setWithObjects:@"selectedViewController", nil]; 。每当 selectedViewController 属性发生变化时,这都会触发 navigationItem 上的 KVO。我不确定UINavigationController 是否正在使用 KVO 观察navigationItem 我不认为 navigationItem 属性会改变,除了第一次调用延迟加载的方法。另外,如果您更新 UINavigationItem 上的属性,我认为不会触发针对 UIViewController 上的 navigationItem 属性的 KVO。 【参考方案1】:

所以我目前实现的解决方案是在 UIViewController 上创建一个类别,其方法允许您设置该控制器导航项的右栏按钮,然后该控制器发布通知,让关心的人知道右栏按钮项目已更改。

在我的容器控制器中,我从当前选定的视图控制器中侦听此通知,并相应地更新容器控制器的导航项。

在我的场景中,容器控制器会覆盖类别中的方法,以便它可以保留已分配给它的右栏按钮项目的本地副本,如果出现任何通知,它会将其右栏按钮项目与其child 的,然后发送一个通知,以防它也在容器控制器中。

这是我正在使用的代码。

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】:

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

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

该属性定义为:

open var navigationItem: UINavigationItem  get  

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

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

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

【讨论】:

很好的解决方案。我认为现在这应该是首选方法。 知道 UINavigationController 是如何知道当前孩子何时改变的吗?在您的情况下,您一次只有一个孩子(添加一个时,前一个被删除)? 是的,关于该问题的第一条评论也暗示了这一点,但在第二条评论中,OP 说当孩子发生变化时,他们想要返回新孩子的导航项目,并且不会再次调用它。 很好的解决方案!但是,UINavigationController 只查询一次navigationItem。如果此时未设置子 VC(例如,在 viewDidLoad 之前),则使用容器 navigationItem。当子 VC 更改时,navigationItem 也无法更新。【参考方案3】:

接受的答案有效,但它违反了 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 的双重通知。它对工具栏项完美无缺!

【讨论】:

UINavigationItem 似乎不是 iOS 7 的 KVO 投诉。我使用 [_selectedViewController addObserver:self forKeyPath:@"navigationItem.rightBarButtonItems" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; 观察导航项上右栏按钮项的更改,并且它仅在初始设置时触发一次。 经过进一步调查,如果您调用 setRightBarButtonItems 方法,它似乎符合 KVO。如果您调用动画方法,例如[[self navigationItem] setRightBarButtonItems:@[ addBarButtonItem, editBarButtonItem ] animated:YES]; 那就不能正常工作了。【参考方案4】:

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

【讨论】:

不幸的是,这是不可能的,因为应用程序的流程是围绕能够将视图控制器推送到导航控制器上而构建的。如果选择了表视图中的任何行,则会将“详细”视图控制器推送到导航堆栈上。此外,子视图控制器不需要知道它在容器控制器内。如果它在 UINavigationController 或任何容器控制器内,它应该只设置它的 UINavigationItem 并工作。 所以用户基本上可以将所有子视图推送到导航堆栈内?然后我可以加载 10 个视图控制器,当我开始返回时,我在应用程序的其他地方?听起来令人困惑,不仅对我们,对用户也是如此。 没有。想象一下,您可以在“今天”、“明天”和“未来 2 天”的项目列表之间来回滑动。这是容器控制器。如果您按下该列表中的任何项目,您会将一个新的 UIViewController 推送到堆栈上,该堆栈将被放置在容器控制器的顶部。如果您按下后退按钮,您将被带回容器控制器,位于您选择项目时的确切位置。这是我正在开发的 theScore 应用程序中使用的容器控制器。直到现在我们才需要导航栏中的项目。 您仍然可以使用自定义 UINavigation 栏,然后在需要时隐藏和显示 UINavigationController 的导航栏。 所以你建议我的容器控制器添加一个 UINavigationBar 到它自己的视图并负责隐藏它所属的任何 UINavigationController 的导航栏?【参考方案5】:

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

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

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

【讨论】:

以上是关于处理容器控制器中子视图控制器的 UINavigationItem 更改的最佳实践?的主要内容,如果未能解决你的问题,请参考以下文章

用两个手指使用 UIScrollView 的自定义容器

如何以编程方式控制 Android AbsoluteLayout 中子视图的大小

使用自动布局垂直居中子视图

在多个视图控制器中重用一个容器视图

如何在容器视图和主视图控制器之间正确传递数据

以编程方式创建带有后退按钮的导航控制器