Making More of the More View

One of the most frustrating things about using a tab bar in your app is that once you exceed five tabs you get the dreaded ‘More’ view - a system component which always displays with stock styling, lacks almost any customisation options, and just seems to have that slight air of desperation about it. In fact the only real way to get the more view consistent with the rest of your app’s design is to create your own view controller, put it in the fifth slot, then start adding the navigation logic for the rest of your app to that manually (which also prevents users editting the tab bar).

But instead of just rolling over and creating our own custom more view, though, I wanted to go ahead and customise the one that Apple already provided. What’s more, I want do it without crazy runtime hackery, without subclassing anything, and with safeguards against future implementation changes in the system components. So, in this post, that’s exactly what we’re going to do.

How we’re going to do this is actually quite simple: we’re going to replace the moreNavigationController’s table view’s delegate with one of our own design1.

After I thought of this, I did find a couple of implementations following the same principle here and there about the web, but they all fell short in a number of ways - especially in handling potential implementation changes in future versions of UITabBarController. Ideally, we want to use the original implementation of the tab bar delegate as much as possible, picking and choosing methods to override to gain our custom behaviour with as few changes as possible, and if things go wrong we want to gradefully degrade to the default more view implementation without any crashes or bugs.

Switching out the delegate is a pretty simple job. We’ll make a class which conforms to the UITableViewDelegate protocol, and assign it as the delegate for the table view. The sketchiest bit is attaining the table view itself, and to guard against the possibility that Apple might stop using a UITableView for the More view in the future, I’ve added a check to make sure it’s the right class. That way, if the default implementation of this view is changed to UICollectionView or something, the app will simply fall back to the default look instead of crashing horribly.

1
2
3
4
5
6
7
8
9
10
11
12
UITabBarController *tabBarController = [[UITabBarController alloc] init];

UITableView *moreTableView = (UITableView *)tabBarController.moreNavigationController.topViewController.view;
if ([moreTableView isKindOfClass:[UITableView class]]) {
  // Set up the table view's properties here
  [moreTableView setBackgroundColor:[UIColor blackColor]];
  [moreTableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];

  // Create the new delegate and store it
  self.tabBarMoreViewDelegate = [[IWSMoreTableViewDelegate alloc] initWithForwardingDelegate:moreTableView.delegate];
  moreTableView.delegate = self.tabBarMoreViewDelegate;
}

The new ‘proxy’ delegate then holds on to the original delegate and forwards any messages it receives directly on to the original delegate. Run this up and you’ll find your more view controller acting exactly like it did before2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface IWSMoreTableViewDelegate ()
@property (nonatomic, strong) id<UITableViewDelegate> forwardingDelegate;
@end

@implementation IWSMoreTableViewDelegate

- (instancetype)initWithForwardingDelegate:(id<UITableViewDelegate>)delegate
{
    self = [super init];
    if (self) {
        self.forwardingDelegate = delegate;
    }
    return self;
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ([[self class] instancesRespondToSelector:aSelector]) {
        return YES;
    }
    return [self.forwardingDelegate respondsToSelector:aSelector];
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.forwardingDelegate;
}

@end

All that remains now is to start implementing table view delegate methods to make alterations to the design of the table view. This is why the above respondsToSelector: implementation returns YES if instances of the current class respond to the selector - otherwise if we tried to implement a UITableViewDelegateProtocol method that the original delegate didn’t implement, our code would never get called.

The most useful method to override here to alter the look of the table view is -tableView:willDisplayCell:forRowAtIndexPath:. Here’s my implementation which changes the more view to display orange titles in Avenir and pink cell images on black backgrounds which turn green when selected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([self.forwardingDelegate respondsToSelector:_cmd]) {
        [self.forwardingDelegate tableView:tableView willDisplayCell:cell forRowAtIndexPath:indexPath];
    }

    cell.backgroundColor = [UIColor blackColor];
    cell.textLabel.textColor = [UIColor orangeColor];
    cell.textLabel.font = [UIFont fontWithName:@"Avenir" size:18];

    cell.imageView.tintColor = [UIColor purpleColor];

    UIView *selectionBackground = [[UIView alloc] initWithFrame:cell.bounds];
    selectionBackground.backgroundColor = [UIColor greenColor];
    cell.selectedBackgroundView = selectionBackground;
}

Beautiful.

It’s worth mentioning that each time you override a delegate method it’s always worth also forwarding the call on to the original delegate (assuming it implements that method, of course). Without this, you may inadvertantly alter the behaviour of the view in ways you didn’t expect.

I’m not going to say that the possibilities for customisation are unlimited, but there’s certainly an awful lot of tweaking which can be done to the cells from inside this proxy delegate, including alterations to the cell layout, height adjustments, colours and fonts and even selection behaviour. What’s more, there’s essentially no real reason you couldn’t pull off the same trick with the table view’s data source, and add custom headers or footers or even entire cells. And all without ever even having to import <objc/runtime.h>.

I’ve put a quick sample project up on GitHub to demonstrate this idea, so please do check it out and let me know what you think.


  1. Now, you might say at this point that this is still definitely a runtime hack. And, well, you may have a point. But to my mind it’s a comparatively elegant one, and it doesn’t do anything beyond leveraging the flexibility of the design patterns and the publicly declared protocols in Cocoa Touch.

  2. Only in software development can doing a whole load of work to achieve exactly the same functionality we had before we started be treated as an achievement.