Elias Sanchez asks:
Hello Oliver. Quick? Do you have any tips in making a toolbar appear/disappear? Trying to mimic what the NYT app does when looking at an article. Is it using Core Animation perhaps? Can’t find anything out there. Any ideas? ThxU
Looking at the NYT App you can see that they do quite a bit of manipulation of the bars of which there are 3: Tab Bar, Navigation Bar and sometimes a Tool Bar.
For beginning iOS Developers it might seem daunting to combine all of these for the effect that the NYT App achieves. In this article I give you an analysis of what they did so that you have their techniques at your disposal, too.
Let’s start out by investigating how the basic navigation views are laid out.
At its root the app has a tab bar (Latest, Popular, Saved, Search, More). Each of the tabs has its own view controller stack. To get the navigation bar and table navigation these tab view controllers have to be navigation controllers with table view controllers as first element on their stack.
You can imagine this like several stacks next to each other where each stack belongs to one tab in the tab bar controller. Because of this, if you navigate around on one tab, the contents of the other tabs are not affected.
So the front page of the article list is a tableview controller (with a header view “Most E-Mailed”) in a navigation controller in a tab bar controller. You see the hierarchy in front of your mental eye?
Now if you tap on an article several things happen at the same time:
- The tab bar at the bottom slides to the left, together with the article list
- The navigation bar becomes translucent
- A toobar slides in from the bottom
The same happens in reverse if you go back. You can hide and unhide both the navigation bar and the toolbar by tapping on the screen while the article always fills the entire view. As a geeky detail there is an ad banner riding on the top of the toolbar and moving with it.
First the Bars
For this tutorial you would prepare a tab bar application from the project template. Then you would remove the FirstViewController and replace it with a UINavigationController which gets a UITableViewController as first child. You can download the project here: NYTBars. Have a look at the MainWindow.xib to see this layout.
The following code will perform the hiding and sliding on the individual article view controller:
- (void)viewDidLoad { [super viewDidLoad]; UIBarButtonItem *item = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add:)] autorelease]; [self setToolbarItems:[NSArray arrayWithObject:item]]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.navigationController setToolbarHidden:NO animated:YES]; self.navigationController.toolbar.barStyle = UIBarStyleBlackTranslucent; self.navigationController.navigationBar.barStyle = UIBarStyleBlackTranslucent; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.navigationController setToolbarHidden:YES animated:YES]; self.navigationController.toolbar.barStyle = UIBarStyleBlack; self.navigationController.navigationBar.barStyle = UIBarStyleBlack; } |
In viewDidLoad we are setting up the items for the toolbar. The toolbar itself is owned by the view controller’s navigation controller i.e. the UINavigationController that this view controller is currently on. But the items you specify via the view controller’s setToolbarItems. These are local to the view controller and every view controller can set it’s own toolbar items. If I’m not mistaken then this methodology was introduced with iOS 3.0. Before that you had to manage the toolbar yourself.
If we look closely at the transition from article list to individual article we see that the animations occur during the slide. This is why we put the showing and hiding in the WILL delegate methods. If we wanted the animations to happen after the slide in then we would instead use the DID methods. viewWillAppear is triggered as soon as the view controller is pushed on the navigation controller and before the animation is over.
So you see how we change the bar style between translucent and opaque and how we show/hide the toolbar with animated:YES. We don’t tell the article view controller that it wantsFullScreenLayout, because this would cause the view to be under the status bar as well, but the same principle would work for hiding this as well.
One thing though: the tab bar sticks around still, so – before pushing the article view controller – we need to tell it that it will also hide the tab bar. The property in question is called hidesBottomBarWhenPushed, admittedly not really intuitive. The “bottom bar” should rather be called “tab bar” in my opinion. I put it in the initWithNibName, but any place before the pushing is fine:
self.hidesBottomBarWhenPushed = YES; |
Previously I had this setting right be fore pushViewController but I prefer to put these things into the objects themselves where possible.
Tap to Hide
To get the interactivity i.e. ability to hide the bars we use a tap gesture recognizer which we attach to the view in viewDidLoad and wire up some more hiding/showing.
- (void)viewDidLoad { // other inits omitted UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; [self.view addGestureRecognizer:gesture]; [gesture release]; } - (void)tapped:(UITapGestureRecognizer *)gesture { BOOL barsHidden = self.navigationController.navigationBar.hidden; [self.navigationController setToolbarHidden:!barsHidden animated:YES]; [self.navigationController setNavigationBarHidden:!barsHidden animated:YES]; } |
This makes for the nice effect to switch between full-screen reading and access to tools or the back button.
Bonus Section: Ad Riding on Toolbar
The toolbar is just a UIToolbar which descends from UIView. For performance reasons the default setting is to not clip subviews. Therefore we might be tempted to add our ad banner like this to the toolbar:
UIView *simulatedAd = [[[UIView alloc] initWithFrame:CGRectMake(0, -30, 320, 30)] autorelease]; simulatedAd.backgroundColor = [UIColor blueColor]; [self.navigationController.toolbar addSubview:simulatedAd]; |
This works, but only while the toolbar is visible. iOS hides the toolbar and by extension all of its subviews when it goes off screen. It does not check whether a non-clipped child view would still be visible. This causes the ad view to disappear as well. Not the affect we were shooting for.
Instead we need to position the add as subview of the article view controller and move it in synchronized fashion whenever the toolbar moves.
How about some fancy pants KVO (Key Value Observing)? Nope, won’t work. No KVO event triggers on key “frame” and the bounds of the toolbar don’t change even though KVO triggers on “bounds” every time the bar moves. And since there is no notification to my knowledge that would inform us about the bar moving this leaves us with only one option: move it ourselves and time it to be in sync with the bar.
Since we know exactly when the bar is coming or going we can simply animated the frame of the ad banner for the same distance and duration. Apple provides the UINavigationControllerHideShowBarDuration constant which gives us the exact timing.
- (void)viewDidLoad { // ... // the +15 is a mystery to me simulatedAd = [[[UIView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height+15, 320, 30)] autorelease]; simulatedAd.backgroundColor = [UIColor blueColor]; simulatedAd.autoresizingMask = UIViewAutoresizingFlexibleTopMargin; [self.view addSubview:simulatedAd]; } - (void)positionAdUp:(BOOL)up animated:(BOOL)animated { CGRect frame = CGRectMake(0, self.view.bounds.size.height-30, 320, 30); if (up) { frame.origin.y -= self.navigationController.toolbar.bounds.size.height; } if (animated) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:UINavigationControllerHideShowBarDuration]; } simulatedAd.frame = frame; if (animated) { [UIView commitAnimations]; } } - (void)viewWillAppear:(BOOL)animated { // ... [self positionAdUp:YES animated:YES]; } - (void)viewDidAppear:(BOOL)animated { // ... [self positionAdUp:YES animated:YES]; } - (void)viewWillDisappear:(BOOL)animated { // ... [self positionAdUp:NO animated:YES]; } - (void)tapped:(UITapGestureRecognizer *)gesture { // ... [self positionAdUp:barsHidden animated:YES]; } |
Like this we can animate the ad around or by calling the positionAdUp method with animated:NO it would pop there. If you didn’t want the ad to already show on slide in, then you could pop it in in viewDidAppear or after the ad is loaded from server.
With this the ad is always perfectly synchronized with the bar. I discovered the +15 in the init through trial and error with slowmo though I cannot explain how this is calculated. For all other other frame moving I’m subtracting the ad banner height from the view height and it fits. If you can come up with a good explanation, let me know in the comments.
Conclusion
If you know how that mimicking the NYT App’s UI is a breeze. Personally I would not put so much UI changes into the user’s face. Especially the hiding of the tab bar in tandem with sliding in a toolbar looks awkward to me. You could just as well wait with the toolbar until the navigation animation is over and then slide it in. Less lines crossing over each other.
As usual, if you read this article to it’s and and learned something from it, then I would like to encourage you to use the Flattr button below to show your appreciation or the social bookmarking feature to share this article with your developer colleagues.
Categories: Q&A