Monday, 20 April 2009

iPhone Tweetie Style Navigation Framework


I love the iPhone Twitter client Tweetie - it has a really excellent navigation framework that makes it fast to navigate between multiple Twitter accounts.



I'm developing an iPhone app that logs actions against one or more persons and thought that Tweetie's navigation style would suit. Tweetie uses a navigation controller to handle the hierarchical nature of browsing account/tweets/users but also uses a tab bar to navigate between various views - a user's tweets, their mentions, DMs etc.  Investigating the Apple documentation, having a tab bar controller within a navigation controller is not a supported Apple user interface approach - the iPhone SDK doesn’t support it and trying results in a blank tab bar when you navigate to the view hosting it. I then found this post, with a comment from Tweetie developer himself, @atebits, alluding to the fact that a custom tab view controller solution is required.

The general steps are outlined below and full source code that can be used as a template for your own projects is posted at GitHub. I recommend you download this now for reference while working through this article.

The App Delegate

The App Delegate holds the navigation controller - this controller is at the root of the view tree and contains any other view controller that is pushed onto it. The navigation bar at the top of the screen that handles the navigation back and forth between views is to a large extent, managed automatically by the navigation controller.

As discussed previously, the issue with current versions of the SDK is that a tab bar controller cannot be pushed onto a navigation controller.

Now, have a look at MainWindow.xib in Interface Builder.

Note that the navigation controller has a “root view controller”. This is the controller that handles the first view that is pushed onto the navigation controller. Opening RootViewController.h reveals that the controller is inherited from UITableViewController. It is fairly typical that the navigation controller oversees multiple levels of table views.
@implementation BabyBookAppDelegate
@synthesize window;
@synthesize navigationController;

The Root View Controller

In Tweetie, the root view controller is the list of Twitter accounts. In my sample code, each table cell is driven from a simple array of strings (people’s names) for demonstration purposes.
self.title = @"Accounts";
NSArray *array = [[NSArray alloc] initWithObjects:@"Jack", @"Jill", @"John", nil];
self.accounts = array;
[array release];
When a cell is clicked, the delegate method didSelectRowAtIndexPath is executed. This pushes the custom tab view controller onto the navigation controllers stack. Note, that this is a custom tab view controller, and does not inherit from the typical SDK class UITabViewController. This means that there is some extra work to manually build this view controller, and also the view it owns.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.tabViewController setTitle:[accounts objectAtIndex:indexPath.row]];
[self.navigationController pushViewController:self.tabViewController animated:YES];
}

The UICustomTabViewController

Open TabViewController.xib. Have a look at the structure of the interface. I've manually created a Tab Bar, which in this case contains the two default tab bar items - Favourites and More. These two buttons have associated view controllers that own the view that appears when a button is clicked. The UICustomTabViewController class manages the switching of the views by maintaining (a) an array of the view controllers and (b) a reference to the selected view controller.


The controller also acts as a delegate for the tab bar, so can respond to taps on the tab bar items through the didSelectItem delegate method. Based on the item tapped, a reference to the appropriate view controller (Favourites or More) is pulled from the array of view controllers. The current view is removed from the superview and the view of the selected view controller is then added to the UICustomTabViewController’s view. Really, the view above the tab bar is getting swapped out for a new view, depending on which tab bar item is tapped.
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
   if (item == favouritesTabBarItem) {
     UIViewController *fabViewController = [viewControllers objectAtIndex:0];
     [self.selectedViewController.view removeFromSuperview];
     [self.view addSubview:fabViewController.view];
     self.selectedViewController = fabViewController;
   } else if (item == moreTabBarItem) {
     UIViewController *moreViewController = [viewControllers objectAtIndex:1];
     [self.selectedViewController.view removeFromSuperview];
     [self.view addSubview:moreViewController.view];
     self.selectedViewController = moreViewController;
   }
)

The More Tab


The MoreTabViewController.m class controls another table view and demonstrates that the navigation controller can be used to manage further views in a hierarchy. The delegate method didSelectRowAtIndexPath will retrieve the navigation controller from the application delegate and push the view controller that handles the next view in the navigation hierarchy - MoreOptionViewController.
NavTabAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
[delegate.navigationController pushViewController:moreOptionViewController animated:YES];
[moreOptionViewController.label setText:msg];
Again, the full source for the NavTab project is on GitHub.

Further recommended reading -

Apple View Controller Programming Guide

41 comments:

  1. Nice example for a view controller framework with a nav bar....thanks for making it available via svn

    ReplyDelete
  2. Hi, Vey nice tutorial, but i'm not able to download the app from gogle checkout, can you make sample code available through diffrent downlaod resource.....thnaks a lot in advance...will be waiting for update n a new download link..

    ReplyDelete
  3. I also can't download the source code. Could you post it as a .zip so we can just download the whole package? Thanks for the great tutorial!

    ReplyDelete
  4. How would you pass the didselectrowforindexpath data along to the subview controllers in order to display information pertinent to the row selected? Currently, it is set up to push the TabBarController only which then controls the subviews. Any ideas would be much appreciated!

    ReplyDelete
  5. Jean-Fran├žois17 July 2009 at 02:21

    Dear Bob,
    Many thanks for sharing this nice piece of code. It works perfectly.
    Just one more question.
    We download a "View only" as a "Viewcontroller" from the nib file. Which mecanism makes this View being handled as a Viewcontroller ? Since the View is part of a Viewcontroller but is not a Viewcontroller (???)
    thanks again.

    ReplyDelete
  6. is there an easier way to get my hands on theses files? Or can someone who has downloaded this already share in .zip file?
    Gracias!

    ReplyDelete
  7. Thanks for the post. I was trying to implement something like this in my app. I followed your instructions and also looked at your source code as examples. However, the problem i'm having now is when my RootView pushes the tab view controller I can't see the tab bar itself. The .xib for the tab view controller is where I put the tab bar. Also, i tried playing with the hidesBottomBarWhenPushed and that's not the problem either.

    ReplyDelete
  8. Thanks for a great tutorial!
    Im a newbee to app development but this tutorial really got me going:)
    I managed to attach a XML to the tableview and then I want to pass a variable from the selected row to the favorite view and put it out in a label. I suppose this could be a stupid question but im really stuck.
    In the didSelectRowAtIndexPath in RootViewController I added this code:
    NSString * storyLink = [[stories objectAtIndex: storyIndex] objectForKey: @"code"];
    [self.tabViewController setTitle:storyLink];
    [self.tabViewController.favlabel setText:storyLink];
    Please help, thanks again!

    ReplyDelete
  9. Robert, this has been great and I have so far successfully used it with my own project but I am in the same boat as Jonah. I see you have answered him but what I would like to know is if I can point to data in a plist using your method? My main nav is populated from a plist and I would like the tab views to be populated with the rest of the data from the row selected. Is it possible before I go down the road?

    ReplyDelete
  10. Thanks so much for this tutorial - it's exactly what I was looking for.
    However, I'm having trouble downloading the source files too. Shouldn't there be a .xcodeproj file?
    Maybe I'm missing something - I am a newbie, if it isn't obvious!

    ReplyDelete
  11. Just as a side note to my previous post. The method (void)viewWillAppear:(BOOL)animate I added on my own as I need a way to fire some code every time the tab view switches.
    Regards,
    Psyc

    ReplyDelete
  12. @Psyc
    I am squeaky new to all this so forgive me if I am not as specific as I would like to be. I was learning about this very thing yesterday in the midst of trying to figure out other issues. My understanding is that the viewWillAppear fires on the root controller of the NavigationController and is not guaranteed to fire on any other controllers in the stack.
    When I added viewWillAppear on the root controller object, it fired for me. I have not done much else as I am still working on implementing the whole design of tab bars and navigation controllers.

    ReplyDelete
  13. Thank you for this tutorial. I have not yet successfully implemented it yet (requires significant tweaking to fit my project), but before I get too far into it I wanted to make sure that using this type of custom workaround won't cause my app's rejection by apple.
    I don't mean to cause offense, I just feel the need to verify this before I move forward. I understand the Tweetie app uses a similar method, and you are obviously accomplished yourself; I'm just taking precautions.
    Thanks again!

    ReplyDelete
  14. Thanks for this great tutorial. It works fine for me but one question is remaining. How can I add another navigation item in the navigation bar (e.g. an edit bar button item)?
    self.navigationItem.rightBarButtonItem = self.editButtonItem;
    added in viewDidLoad is not working (this same approach in MoreOptionViewController shows the edit button). I suppose this came from using the app delegate's navigationController in MoreTabViewController inside didSelectRowAtIndexPath .

    ReplyDelete
  15. @quigley
    The example doesn't use any unsupported/unpublished APIs or classes and in my mind it makes sense for certain UI applications - as you mention is is used in Tweetie, and it makes sense that the NavigationController is the root so that you can select an account before moving into the main function.
    I'd think your risk is minimal (especially considering Tweetie is up there) but then again I don't speak for Apple!

    ReplyDelete
  16. @Robert
    Thanks for your reply. Your code example is working in viewDidLoad on RootViewController.m, MoreOptionViewController.m and UICustomTabViewController.m without navigationController in the second last line. So it looks like in my case:
    UIBarButtonItem *editBarItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:nil];
    self.navigationItem.rightBarButtonItem = editBarItem;
    [editBarItem release];
    However, adding it into UICustomTabViewController.m shows the edit button for both tab bar selections - Favorites and More. But for example if I wanted this button only after Favorite selection this code block is not working properly on FavoritesTabViewController.m inside viedDidLoad. What am I doing wrong here? The same problem is given by setting the navigation's bar title: self.title = @"favorite" is ignored in viedDidLoad on FavoritesTabViewController.m.
    Please help, and thanks again!

    ReplyDelete
  17. Okay, I got around this problem by writing the relevant edit button code (and self.title) inside UICustomTabViewController's initWithNibName:bundle and tabBar:didSelectItem methods.

    ReplyDelete
  18. This was a big help for me as well. Much appreciated.
    As @psyc pointed out, viewwillappear() won't get called if you implement it in your MoreTabViewController. The problem is exactly what AC pointed out: viewwillappear() is being called for the "root" controller object which in this case is your UICustomNavViewController. Because viewwillappear() is NOT being called on the MoreTabViewController (which should then call [super viewWillAppear: animated]), the selected item is not being deselected when you navigate back from a leaf node to the moreTabViewController.
    The solution I'm using is to implement the viewWillAppear method in UICustomNavViewConroller as:
    - (void) viewWillAppear:(BOOL)animated
    {
    [self.selectedViewController viewWillAppear:YES];
    [super viewWillAppear:YES];
    }
    Which gets the viewWillAppear message to the moreTabViewController, and the super viewWillAppear of moreTabVIewController makes sure that the selected item is deselected when it reappears.
    Thanks again for the pointers!

    ReplyDelete
  19. Hi Wired Bob,
    Im struggling here - could you tell me how you start the app in Xcode - i.e. what type of project template is best to start with - a Windows based App or Nav or Tab bar?
    I would appreciate your guidance on this.?
    THis would be much appreciated - otherwise love what you have done here so thanks.

    ReplyDelete
  20. Hi,
    I played around with different navigation bar colors for the Favorite and More tab bar views. This works accordingly for setting colors with tintColor method in viewWillAppear and tarBar:didSelectItem like so:
    self.navigationController.navigationBar.tintColor = [UIColor colorWithRed:1.0f green:0.0f blue:0.0f alpha:1.0f]; // red for Favorite
    and
    self.navigationController.navigationBar.tintColor = [UIColor colorWithRed:0.2f green:0.4f blue:1.0f alpha:1.0f]; // blue for More
    But there is one problem remaining. The navigation bar back button isn't colored correctly but represents the last corresponding color after a tab bar's selection. This means if Favorite was chosen, the back button color is also red in the 'More' tab view. This can only be changed to blue color if one goes back to the root view and reselects an entry. But I would like to have set the correct button color after hitting a tab bar button or after selecting a name in the root table.
    Is there anyone who knows how to fix this problem?
    Thanks in advance
    steve

    ReplyDelete
  21. Hi Wired Bob,
    I am interested in understanding if you could design and code a template for a similar but different application for me.
    I have all the Gfx and content and a design tree ready.
    Could you please email me to discuss
    Thanks
    Zak

    ReplyDelete
  22. Hi,
    Lame question - sorry! Follwed this through and added a new tabBarItem, created a view and added to the UICustomTabViewController custom initialization and array but when i click on the tab item the new view is not loaded above the tabBar (tabBar disappears)
    What am i missing?
    Thanks,
    Mike

    ReplyDelete
  23. thanks a lot

    ReplyDelete
  24. Hi Robert,
    I tried to edit your source code to add more tabs to it and then, and added it to my project. The problem is it won't work! This is the error that I got:
    error: request for member 'tabViewController' in something not a structure or union
    It's from this part of code:
    UICustomTabViewController *tvController = [[UICustomTabViewController alloc] initWithNibName:@"TabViewController" bundle:nil];
    self.tabViewController = tvController;
    [tvController release];
    Any help would be appreciated.
    Thanks.

    ReplyDelete
  25. Hi Robert,
    I have the same structure in my App and it works great. But one thing makes me crazy :(
    When I load a ModalView in one of the Tabs, this View hides the TabBar because the TabBar is
    just a SubView. How can I realize that only the part between the NavigationBar and the TabBar
    turns around, so that the two Bars are still visible?
    Thanks in advance
    njaa

    ReplyDelete
  26. I call [self presentModalViewController:aController animated:YES] from one of my tabs. When I dismiss the modalViewController, the tab bar has disappeared! Can anybody help me fix this?

    ReplyDelete
  27. Help, my tabBar is failed to appear when load into the tab bar view, anyone know why and help me solve the problem please...

    ReplyDelete
  28. This is a really nice tutorial. For some reason my UITabBarItems don't become active and only the initial set one is selected without giving me the option to select any other. Has this happened to you? If so, how did you solve this?

    ReplyDelete
  29. Thanks a lot for the great post, Robert!
    I'm using this approach in my own application, but I found that this method is actually abusing UIViewControllers. Here is full info: Abusing UIViewControllers.
    I really want to see what is the supported way for creating such navigation framework without abusing UIViewControllers. Do I need to create UITabBarController and then incorporate UINavigationController and then disable UITabBar on all pages except on two non-main paged down in hierarchy? But that’s ridiculous.

    ReplyDelete
  30. PLS READ THIS Comment
    I have read your NavTab application . It's very helpful for me but i have one question in my mind for your NavTab
    .."application if i want to add some other tabs in your application.like you have implemented favorites , more etc.
    I have implemented one extra tabbaritem named others . it has successfully implemented but when i click on the others tabbaritems than the view of the others is going to be called but i lost the tabbar which appears in all favorite and more menu. but in my others view tabbar disappears.. ....!!"
    What should i do to see the tab bar like it is going to see in more and favorite tabbar items..??
    pls help me..

    ReplyDelete
  31. One more question Can i change the view of tab bar items and tab bar. like i want to create tab bar in green other than the original color of the tab bar. and when the tab bar item has been selected at that time it should be yellow not than the original blue color..
    i have read many comments that the tab bar color and the tab bar items color can't be changed..
    but i want to be sure so i am asking.?
    pls guide me..

    ReplyDelete
  32. I could not figure this out for the loooooongest time (either the tab bar or the subview would show, but not both), even though I was following the sample code in my project, until I finally figured out I needed the following, rather than calling addSubview :
    [self.view insertSubview:tab2ViewController.view belowSubview:myTabBar];
    (credit to a comment on here: http://stackoverflow.com/questions/576764/tab-bar-controller-inside-a-navigation-controller-or-sharing-a-navigation-root-v )
    Hoped that helped somebody! I notice a lot of you were having trouble getting it to work, maybe it's this?

    ReplyDelete
    Replies
    1. Thanks this helped a lot. Fixed my disappearing tab bar.

      Delete
  33. Thanks for this. I had been fighting with doing this for a half hour, searching google for a half hour, repeat, until I came across this post. It is exactly what I needed. I know a lot of people who say you should never do tab controller in a nav controller, but it is sometimes the exact right solution and this helps describe how.

    ReplyDelete
  34. hey did you get a chance to see about downloading the tutorial had a bit of problems downloading it. thank you for the great tutorial.
    Regards
    Billy

    ReplyDelete
  35. Just trying to contribute with my one cent...
    The original code doesn`t work well when rotation occurs and you need your tab views to respond accordingly to orientation rotation. So I recommend to add the views from your tab`s VCs as subviews (in -initWithNibName) and manage the hidden property of the views in the -tabBar:didSelectItem: delegate method.
    Thus, when TabViewController gets rotated, the tab views will rotate too. Note that their VCs won`t get callbacks, though.
    Hope this helps

    ReplyDelete
  36. shitty app.!
    It uses tabbar instead of tabbarController which was the major problem to be considered.

    ReplyDelete
  37. what about if the application is created from a tab bar aplication(from the templates) not navigation application.. tnx for the reply

    ReplyDelete
  38. This is very helpful. Do you have a similar one for xCode 4.3 using a storyboard?

    ReplyDelete
  39. Thanks for such a nice tutorial. I am having a little problem in nib file while following your source code for my scenario. I can't find delegate option in Referencing Outlet when I click on File's Owner in Connections Inspector. Please can someone tell me what am I missing? Thanks alot...

    ReplyDelete