Best practices for handling changes to the UINavigationItem of child view controllers in a container controller?

So the solution that I have currently implemented is to create a category on UIViewController with methods that allow you to set the right bar buttons of that controller's navigation item and then that controller posts a notification letting anyone who cares know that the right bar button items have been changed.

In my container controller I listen for this notification from the currently selected view controller and update the container controller's navigation item accordingly.

In my scenario the container controller overrides the method in the category so that it can keep a local copy of the right bar button items that have been assigned to it and if any notifications are raised it concatenates its right bar button items with its child's and then sends up a notification just incase it is also inside a container controller.

Here is the code that I am using.

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

I know this question is old, but I think that I found the solution for this problem!

The navigationItem property of a UIViewController is defined in a category/extension in the UINavigationController header file.

This property is defined as:

open var navigationItem: UINavigationItem { get } 

So, as I just found out, you can override the property in the container view controller, in my case:

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

I tried this approach and it's working for me. All buttons, title and views are being shown and updated as they change on the contained view controller.


The accepted answer works, but it breaks the contract on UIViewController, your child controllers are now tightly coupled with your custom category and must use its alternative methods in order to work correctly... I had this issue using the RBStoryboardLink container, and also on a custom tab bar controller of my own, so it was important it would be encapsulated outside of a given container class, so I created a class that has a mirrorVC property (usually set to the container, the one who will listen for notifications) and a few register / unregister methods (for navigationItems, toolbarItems, tabBarItems, as your needs see fit). For example when registering/unregistering for 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];
}

The observe action will handle receiving the new values and forwarding them to the 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];
    }
}

Then in your container, at the right moments, you register and unregister

[_selectedViewController unregister...]
_selectedViewController = selectedViewController;
[_selectedViewController register...]

You must be aware of a potential pitfall though: not all desirable properties are KVO compliant, and the ones that do aren't documented to be - so they can stop being or misbehave at any time. The toolbarItems property, for example, is not. I created a UIViewController category based on this gist ( https://gist.github.com/brentdax/5938102 ) that enables KVO notifications for it so it works in this scenario. Note: the gist above wasn't necessary for UINavigationItem, iOS 5~7 sends out proper KVO notifications for it, with that category I would get double notifications for UINavigationItems. It worked flawlessly for toolbarItems!