If you read
my earlier tutorial about how to create a UITabBar manually you learned to how to programmatically add a UITabBarController without having to use interface builder. While this approach works well for most uses, it does not give the full flexibility that you might want to be able to exercise 100% total control over your UITabBarController. An example of some things that you might want to do would be change the tab bar items "in-flight", or to even do things like move the tab bar to a different location on the screen, or be able to easily show or hide the tab bar. In short, by manually creating a tab bar as I will show in this tutorial, you can pretty much exercise full control over it and make it do whatever you want.
As part of this tutorial I'll also show how you can fully customize your view and show a UITableViewController embedded in a UINavigationController embedded in a UITabBarController on only half the screen. And then I'll put some buttons and such on the top half of the screen. You can use this approach to show "mixed" content and views in your application as you desire. When you are done with this tutorial you'll have something that looks like the screenshot on the left. As with anything like this, there are probably many ways you could accomplish this, and this is just one way.
So let's get started. The first thing you'll want to do is create your project in XCode. You are going to do all the work here without any help of Interface Builder, so you can just start with a simple "Window-Based Application" (in XCode... File > New Project > iPhone OS Application > Window-Based Application". Click on "Choose..." and then name your project whatever you want (I called mine "TabMaster").
XCode will create the project and the TabMasterAppDelegate header and body files for you. You always need to have the AppDelegate file as the "launching off point" for any app. In our app, we aren't going to get too fancy within the AppDelegate class. We are just going to initialize our "root" view controller and add it to the application window. In the previous tutorial, I did a lot more within the app delegate class, like creating the navigation controller and tab bar controller. However, in this example I am just going to hand everything off to the root view controller, so to speak, and create everything within that view. This gives me the flexibility to add or remove whatever I want from that view controller, which will come in handy later.
Go ahead and create your root view controller by selecting File > New File... > UIViewController subclass and naming it whatever you want. I called mine "RootViewController". Within the TabMasterAppDelegate.h file, add a forward class declaration for your RootViewController, and then add an instance variable called rootViewController, and declare it as a property. Also remove the IBOutlet type from the property declaration of the window instance variable since we aren't using Interface Builder. Your header file should look something like this, minus the #import stuff at the top:
@class RootViewController;
@interface TabMasterAppDelegate : NSObject {
UIWindow *window;
RootViewController *rootViewController;
}
@property (nonatomic, retain) UIWindow *window;
@property (nonatomic, retain) RootViewController *rootViewController;
@end
Next, within the TabMasterAppDelegate applicationDidFinishLaunching body (.m) file, you'll want to add the following code:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// now you will specify the view controller to show when the application launches.
// keep this generic and you can modify the content within this view controller
// as you'd like (eg. to show different content based on a user preference for
// example
rootViewController = [[RootViewController alloc] init];
[window addSubview:rootViewController.view];
[window makeKeyAndVisible];
}
Also, don't forget to include the RootViewController.h file at the top of your TabMasterAppDelegate.m file, or you'll get a compile error. In this code we are doing a couple of things. First, we are defining the "window" in which this application will run. In this case is the entire iPhone screen, minus the status bar at the top (a rectangle with an origin at (0,0) that is 320 pixels wide and 480 pixels high). See my "iPhone Screen and Application Frames" post for more info on this and also how to hide the status bar if you'd like. This window is now our "canvas" in which we will "paint" whatever views we want (I like that metaphor, don't you?). Also, make sure you release the rootViewController in your dealloc method at the bottom.
In this same block of code, we then instantiate an instance of our root view controller and add its view to this window. For now, this view is just empty because we haven't added anything to it. Next we'll add a UITabBarController and some other things to it. Note that we are doing this in the root view controller instead of in the TabMasterAppDelegate as we did in the my earlier tutorial. This allows us to exercise more control over the UITabBarController since it will just be a subview in our view controller. So we are now done with everything that we need to do in the AppDelegate class. Now everything is going to be handled by the RootViewController. Let's move on to our RootViewController and do the "real" work!
Updated comment: You actually have just as much control doing all of this in the AppDelegate class. Moving everything to the RootViewController is more just a preference than anything else and you could get it to work both ways.
Before we do that though, let's create two UITableViewControllers that will be used in our example. For this tutorial, they won't really do anything, they'll just display empty rows in a table. In your real-life application, these could be table view controllers, or just view controllers or whatever.
From within Xcode, choose File > New File.. > UITableViewController subclass and name your table view controllers "FirstViewController" and "SecondViewController" (or whatever you want, just replace your file names where appropriate). I'm going to borrow some code from my first UITabBarController tutorial, so within each UITableViewController add a method called "initWithTabBar". The .h file for each class should look something like this:
@interface SecondViewController : UITableViewController {
}
-(id)initWithTabBar;
@end
Now in the .m file for each of these classes add the following to define this method within the class @implementation block:
-(id) initWithTabBar {
if ([self init]) {
//this is the label on the tab button itself
self.title = @"Prefs";
//use whatever image you want and add it to your project
self.tabBarItem.image = [UIImage imageNamed:@"prefs_icon.png"];
// set the long name shown in the navigation bar
self.navigationItem.title=@"Preferences";
}
return self;
}
I won't go into too much detail here as this is all from my previous tutorial. One more thing...in the SecondViewController implementation block, modify the initWithStyle method as follows:
- (id)initWithStyle:(UITableViewStyle)style {
if (self = [super initWithStyle:style]) {
// set the long name shown in the navigation bar
self.navigationItem.title=@"Tab Bar Gone";
}
return self;
}
You'll see how this is used a little later on. And don't forget to uncomment out this method in the stubbed out class file.
That's all you need to do for these two UITableViewController classes for this tutorial. Now let's go into our RootViewController to finish up. For this example you don't need to change anything in the RootViewController.h file, so let's dive right into the implementation of the class (the .m file).
Remember back in the app delegate file we added the view for the RootViewController to the window. If you compile your code now (go ahead and make sure it all works), you should get a nice black screen with the status bar at the top. That's because we haven't yet initialized the view for the view controller, or assigned a background color, or done anything else for that matter. So that's why you see nothing. So let's initialize the view and give it a background color. The lion's share of the work here will be in the loadView method. In the loadView method add the follow block of code:
// create the main view
UIView *contentView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
contentView.backgroundColor = [UIColor darkGrayColor];
self.view = contentView;
[contentView release];
// we'll add some more stuff down here shortly
Those four lines up there just define the size of the view (via its frame property), then we give it a nice dark gray background color, and then we set the view property of the view controller equal to this view instance we just created (named contentView). This is all fairly boilerplate stuff that you will do many times in your iPhone developer lifetime. Don't forget good memory management by releasing the contentView which you no longer need. So now go ahead and compile your project yet again, and hopefully this time you should see a dark gray screen instead of a black screen. If you don't see that, then go back and check your code for errors.
In this next section I am going to now add all of the UI elements that you see in the screen shot up at the top. We will add a UILabel for the orange text at the top, a couple of UIButtons (although one of them will be hidden for now). Here's the code to add to your loadView method:
// add the title at the top of the screen
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(10.0f, 10.0f, 300.0f, 35.0f)];
[titleLabel setBackgroundColor:[UIColor clearColor]];
[titleLabel setTextAlignment:UITextAlignmentCenter];
[titleLabel setFont:[UIFont boldSystemFontOfSize:15.0]];
[titleLabel setTextColor:[UIColor orangeColor]];
[titleLabel setText:@"All Your Tab Bars Are Belong to Us!!"];
[self.view addSubview:titleLabel];
[titleLabel release];
// Add the "Remove Tab Bar" button
UIButton *removeButton;
removeButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
removeButton.frame = CGRectMake(20.0, 166.0, 280.0, 37.0);
[removeButton setTitle:@"Remove tab bar" forState:0];
[removeButton addTarget:self action:@selector(makeTabBarGoAway) forControlEvents:UIControlEventTouchUpInside];
// use the Tag property to be able to easily retrieve this subview later on
[removeButton setTag:100];
[self.view addSubview:removeButton];
// Add the "Add Tab Bar" button
UIButton *addButton;
addButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
addButton.frame = CGRectMake(20.0, 166.0, 280.0, 37.0);
[addButton setTitle:@"Show tab bar" forState:0];
[addButton addTarget:self action:@selector(putTabBarBack) forControlEvents:UIControlEventTouchUpInside];
// initially hide this button
[addButton setHidden:YES];
[addButton setTag:200];
[self.view addSubview:addButton];
I won't go into a lot of details on how to programmatically create UIButtons and UILabels, but I did want to give special mention to the "tag" property. Since UIButtons and UILabels are just sub-classes of a UIView, they inherit the UIView "tag" property. By assigning an integer to the view tag property, you can easily retrieve this view later when you need it. The number that you assign to this property is arbitrary, just keep track of what number you assign to what view so you can retrieve it correctly. You'll see how I do this shortly.
Now I'm going to add a bunch to code to create our UITabBarController. Almost all of this comes from my prior tutorial, so I am not going to explain it in detail here. Here's the code to create a simple 2-tab UITabBarController, which will contain our FirstViewController and SecondViewController (also add this inside your loadView method of RootViewController):
// add the tab bar controller
UITabBarController *tabBarController;
tabBarController = [[UITabBarController alloc] init];
// define a custom frame size for the entire tab bar controller that will be in the
// bottom half of the screen.
CGRect tabBarFrame;
tabBarFrame = CGRectMake(0, 260, 320, 200);
tabBarController.view.frame = tabBarFrame;
NSMutableArray *localControllersArray = [[NSMutableArray alloc] initWithCapacity:2];
// setup the first view controller
FirstViewController *firstViewController;
firstViewController = [[FirstViewController alloc] initWithTabBar];
// create the nav controller and add the root view controller as its first view
UINavigationController *localNavigationController;
localNavigationController = [[UINavigationController alloc] initWithRootViewController:firstViewController];
// add the new nav controller (with the root view controller inside it)
// to the array of controllers
[localControllersArray addObject:localNavigationController];
// release since we are done with this for now
[localNavigationController release];
[firstViewController release];
// setup the second view controller just like the first
SecondViewController *secondViewController;
secondViewController = [[SecondViewController alloc] initWithTabBar];
localNavigationController = [[UINavigationController alloc] initWithRootViewController:secondViewController];
[localControllersArray addObject:localNavigationController];
[localNavigationController release];
[secondViewController release];
// load up our tab bar controller with the view controllers
tabBarController.viewControllers = localControllersArray;
[localControllersArray release];
// give the tabBarController view a tag so we can retrieve it later.
tabBarController.view.tag = 1996;
[self.view addSubview:tabBarController.view];
The thing to notice here is that I defined a smaller frame size for the UITabBarController than one would normally do. In this case the origin is at (0, 260) which is about half-way down the iPhone screen, and the height is only 200 pixels. If I made the height any larger, the bottom of the tab bar would be off the screen. Note that with this method you can now put your UITabBarController wherever YOU want to on the screen! You are now the master of the tab bar controller. Another thing to make sure you understand is that the UITabBarController is not just the tab bar at the bottom, but it also "holds" the UINavigationController which holds the UITableViewController you created. I've seen some people get confused and think that the UITabBarController is only the bottom tab bar - it is not. Also notice that I also added a tag to the tab bar controller for easy retrieval later. If you want you can go ahead and compile this now and you should be able to see a screen that looks very similar to our finished product. Make sure you include FirstViewController.h and SecondViewController.h at the top of your RootViewController.m file, by the way, before you compile. You can play around with the tab bar controller buttons and see how it switches between table views, but don't click on the "Remove Tab Bar" button yet, or the program will crash because it tries to call a method that doesn't exist. Note that you also added an "addButton" to the view, but you set its "hidden" property to YES, so you don't see it. But it's there, hiding.
Ok, we're getting closer to finishing this. Before we do any more coding, let me explain the approach here. As it stands now, we've got a view, with a bunch of subviews added to it. One of those subviews just happens to be a UITabBarController in all of its glory. In order to "hide" the tab bar at the bottom, what we are really going to do is hide the entire UITabBarController view, and replace it with a view (from a UITableViewController) that doesn't have a tab bar. To the user it appears that just the tab bar disappeared, but really the entire subview disappeared and was replaced by a totally new subview. Hopefully that makes sense. Also, see my note below about the "hidesBottomBarWhenPushed" property, since some of you may be wondering why I just didn't use that.
So back in the loadView method, add the following code to create our new tab bar-less UITableViewController view. In this example I happened to use our SecondViewController again. I also thought I'd change the color of the navigation bar since somebody asked how to do that once, so the navbar is a nice ugly red in the example.
// create the view to show without the tab bar. For this example just assume it is the
// secondViewController (the "Edit" view). We could also do this without a nav controller
UINavigationController *secondNavController;
SecondViewController *newViewController;
// notice we use initWithStyle here instead of initWithTabBar
newViewController = [[SecondViewController alloc] initWithStyle:UITableViewStylePlain];
secondNavController = [[UINavigationController alloc] initWithRootViewController:newViewController];
secondNavController.view.tag = 1997;
// make the frame size the same as the one we are replacing (with the tab bar)
CGRect navControllerFrame;
navControllerFrame = CGRectMake(0, 260, 320, 200);
secondNavController.view.frame = navControllerFrame;
// hide this view initially
[secondNavController.view setHidden:YES];
// make the tab bar red - you can comment this line out if you don't like it
secondNavController.navigationBar.tintColor = [UIColor redColor];
[self.view addSubview:secondNavController.view];
[newViewController release];
Most of this code should be self explanatory. Note that I once again used a view tag.
So that's all the work we need to do now in the loadView method. If you compile and run the code now it won't look any different because the view we just added is hidden for the time being. Now let's add the code to switch out the views. In the code where we put the UIButtons on the screen, there is a line of code that looks like this:
[removeButton addTarget:self action:@selector(makeTabBarGoAway) forControlEvents:UIControlEventTouchUpInside];
This line says that when the user clicks on the "removeButton", call the method call within RootViewController (aka "self") called "makeTabBarGoAway". You've probably figured out that we haven't yet created a method called makeTabBarGoAway, so that's why your app will crash if you dare press that button at this point. So let's create that method. Note that we don't need to declare it in the @interface section of RootViewController because it is private to the class and can't be accessed by any other classes. Add this code for our makeTabBarGoAway method:
- (IBAction) makeTabBarGoAway {
// hide the view with the tab bar
UIView *viewToRemove;
viewToRemove =[self.view viewWithTag:1996];
[viewToRemove setHidden:YES];
// show the view without the tab bar
UIView *viewToShow;
viewToShow = [self.view viewWithTag:1997];
[viewToShow setHidden:NO];
// show the "Show Tab Bar" button
UIView *buttonToShow;
buttonToShow = [self.view viewWithTag:200];
[buttonToShow setHidden:NO];
// hide the "Hide Tab Bar" button
UIView *buttonToHide;
buttonToHide = [self.view viewWithTag:100];
[buttonToHide setHidden:YES];
}
From reading through this you can probably figure out what is going on. Basically what we are doing is setting all of the "hidden" views to be displayed, and hiding all of the non-hidden views (except for the UILabel at the top), all via the "hidden" property of each (sub)view. Note that we retrieve the subview from the parent view by just using the viewWithTag method. You could also retrieve it by the index of the view, since the subviews are just an array of views. But that requires you to keep track of the order you added each subview and seems prone to error. So all we do in this method is:
1) Hide the view that is the UITabBarController
2) Un-hide the view that is just the UITableViewController without the tab bar controller.
3) Hide the "Remove tab bar" button.
4) Un-hide the "Show tab bar" button.
For the "Show tab bar" button, you'll notice that we call a method named, believe it or not, "putTabBarBack". It basically does exactly the opposite of what "makeTabBarGoAway" does. Here's the code:
-(IBAction) putTabBarBack {
// hide the view without the tab bar
UIView *viewToRemove;
viewToRemove =[self.view viewWithTag:1997];
[viewToRemove setHidden:YES];
// show the view with the tab bar
UIView *viewToShow;
viewToShow = [self.view viewWithTag:1996];
[viewToShow setHidden:NO];
// show the "Hide tab bar" button
UIView *buttonToShow;
buttonToShow = [self.view viewWithTag:100];
[buttonToShow setHidden:NO];
// hide the "Show Tab Bar" button
UIView *buttonToHide;
buttonToHide = [self.view viewWithTag:200];
[buttonToHide setHidden:YES];
}
If you understand the first method, then this one should be pretty self-explanatory. After you have added both of those method calls, you should be able to compile and run the app. If you did everything correctly then it will be a fully functioning app. Click on the button and you'll see the tab bar magically appear and disappear.
Good luck and I hope this was useful (or at least entertaining) for some of you. I've got some pending tutorials around data management, phone rotations, and more tab bar fun (like tab bar items, customization, etc.) that I hope to get up. Or let me know what kinds of things you want to see tutorials on!
One More Thing...
Well, actually a couple of more things. Some of you may know about the hidesBottomBarWhenPushed property that is the recommended way to hide the tab bar. I spent a long time playing with this property and it does appear to work for some people, but I could never get it to behave exactly how I wanted. I was able to successfully "slide" the tab bar out of the view (upon rotation, in my case), but when I rotated the phone back to portrait mode and attempted to slide the tab bar back in, it never worked and I was left with a blank tab bar still stuck in landscape mode running up the left side of the screen. Using this approach I would just swap out the tab bar controller view with my view that doesn't have a tab bar controller and I don't need to worry about the idiosyncratic hidesBottomBarWhenPushed property.
Also, as I was doing this I was trying to understand how the subviews are stored/represented in the main view. As I mentioned above, the subviews are just an array of views stored in the main view. In this example, we have 5 views: 1 UILabel, 2 UIButtons, 1 UITableViewController, and 1 UITabBarController. If I add the following code:
NSArray *arrayOfSubViews;
arrayOfSubViews = [self.view subviews];
at the end of my loadView method and then examine the arrayOfViews in the debugger, I get the following:
Printing description of arrayOfSubViews:
{type = mutable-small, count = 5, values = (
0 : UILabel: 0x52a3c0
1 : UIRoundedRectButton: 0x52de70
2 : UIRoundedRectButton: 0x52e960
3 : UILayoutContainerView: 0x52ee40
4 : UILayoutContainerView: 0x533de0
)}
Note that objects 3 and 4 are of type UILayoutContainerView. These are the views for the tab bar controller and table view controller, interestingly enough. A little research shows that this is an undocumented class, although you can find the class documentation on Erica Sadun's site
here if you are interested. I didn't dig too much further because it appears if you try to dive into UILayoutContainerView and
manually change stuff inside it (like the tab bar itself), it just gets overwritten anyway and doesn't work. So for now it remains an interesting discovery, but not of much value I think. But let me know if you can add any insight into this mysterious class.
12 comments:
Lovin these non-application builder based tutorials, the first one in this article I have found invaluable. Having some problems with this one however:
In TabMasterAppDelegate you have a property rootViewController that doesn't appear to be used. This clashes with a local variable in applicationDidFinishLoading.
In applicationDidFinishLoading you create a local instance of a RootViewController add it as a subView of the window and then release it. If I leave the release in, the view does not display, if I don't release the local instance of RootViewControllor the view displays correctly.
Have I missed something?
Also, why do you create a new window when one is defined for us by MainWindow.xib?
Instead of declaring the root view controller as a local instance, it's better to retain the root view controller to the class instance variable, and add that view as a subview of window.
He's not using MainWindow.xib, that's why he's creating his own window.
Not only is it better to retain the root view controller in the class instance, it seems it's essential to make it work. This tutorial as it stands doesn't seem to work.
I removed the RootViewControler * declaration from applicationDidFinishLoading and moved the [rootViewController release] into dealloc. The instance variable has already been declared but wasn't being used.
I know mainWindow.xib isn't being used (as such) but it does provide the glue between main and app delegate, and it creates a window object, so is there any reason why we can't just use that?
Thanks for those comments and they are all correct. I'm not sure how that rootViewController issue snuck in there since I try to run the code as I do the tutorial (my original source code has this correct). In any case, I fixed the code and it should all work now. Thanks again for your comments!
One more thing about the window. You can leave that out since mainWindow.xib will take care of it. I left it in there out of habit from the old days pre-IB, but you don't really need it anymore and the demo works fine if you take it out altogether. In fact, in 3.0 you must remove that line or your code won't work.
There's problems if this containment is used on a tabcontroller that allows for view rotation.
Nice work here.
I found that all my apps that use "initWithTabBar" do not work on iPhone 3.0 SDK 5. Poking around to see if other dev methods will I tried this code and it too will not load it's views on 3.0.
Just thought I would give you a heads up. Still trying to figure this out.
Brian- I just recompiled and ran using initWithTabBar without any issues. If you are still having problems download the source code and see if it works.
So, is this mean that hidesBottomBarWhenPushed will never work with OS 3.0 below? Or you already found out a way to use hidesBottomBarWhenPushed now?
I have the code which require to use UITabBarController and I want to hide it.
Julie-I saw in either the 3.0.1 or 3.1 release notes that hidesBottomBarWhenPushed now works reliably. I haven't tried it out, but you may want to give it a whirl.
Hey,
I have been following your tutorial on UITabbar. Its a nice practice to create the tabbar programatically.
I just had one issue regarding the navigation Bar though, perhaps I'm missing something obvious here but If I want a simple navigation bar and not on half of the screen? Please suggest me and please continue the good work.
Thanks for the tutorial, but I've noticed an issue when adding a Tab Bar Controller as a subview of a Root View controller -- viewWillAppear is no longer called on the individual views.
Open the tutorial, uncomment "viewWillAppear" in SecondViewController, add a breakpoint and an NSLog line or something, then execute - you can view the "prefs" table w/o the breakpoint tripping.
Any ideas on this? Had a lot of trouble today, found this post on viewWillAppear, but not much help. The only way I can get it to work is to forget about the view and just add the tabBarController as a subview of the window, not another view.
Post a Comment