Building multiple JOIN conditions with Propel's new query API

Or: How Propel got its love back ;)

I recently updated a software project I started in autumn 2008. It was developed using symfony 1.1 with Propel 1.2 as ORM. Back then, we were stucked with PHP 5.1.6 (blame RHEL for that) and I didn’t really had the chance to update to symfony 1.2 when the development cycle for 1.1 ended in summer 2009. This year we eventually had the chance to update to PHP 5.3 which broke my project due to a conflict with mysqlnd and Creole, Propel’s former database abstraction layer. So, I had no more excuses for not updating to a more recent version of symfony.

I also thought about trashing Propel in favor of Doctrine, because Propel 1.2 was slow, buggy and the Criteria API for building database queries was a real PIA. The downside of switching was that it would have taken me weeks to replace every piece of Propel code with Doctrine, so I tried to postpone this decision as good as I could ;)

Meanwhile, François Zaninotto took the lead for the (almost dead) Propel project and brought it back to life. He recently released version 1.5, which feels like a christmas present for all developers that have to deal with Propel legacy code. It features a whole new API for building queries. It can’t be denied that this API resembles Doctrine’s DQL, but it’s really well done, elegant (maybe a bit more elegant than DQL) and blazingly fast. But the best aspect, for my part, is that version 1.5 is fully backward compatible to older versions of Propel. The new query API was build on top of the old Criteria API so it’s up to you to use the old style, the new one, or a mix of both.

After playing around for a while, I was really getting into the new query API. But there was one thing I couldn’t figure out. With Doctrine, it’s really easy to build a query with a complex join condition like this:

$q = Doctrine_Query::create()       
  ->from('Book b')       
  ->leftJoin('b.Author a WITH a.name = ?', 'Douglas Adams')       
  ->where('b.title LIKE ?', '%Hitchhiker%');            

$bookList = $q->find();

which produces the following (or a similar) raw MySQL query:

SELECT b.*     
FROM books b     
LEFT JOIN authors a ON a.id = b.author_id AND a.name = 'Douglas Adams'     
WHERE b.title LIKE '%Hitchhiker%'

How can this be archieved with Propel’s new query API?

$q = BookQuery::create()       
  ->leftJoin( /* what goes here?!? */ )       
  ->where('Book.Title LIKE ?', '%Hitchhiker%');            

$bookList = $q->find();

There’s no such thing as the WITH keyword in DQL. Since the relation itself is resolved automatically by Propel, there’s no chance to add a condition inside the leftJoin method. So, here’s what you will end up with:

$q = BookQuery::create()       
  ->leftJoin('Book.Author')       
  ->where('Book.Title LIKE ?', '%Hitchhiker%');            

$bookList = $q->find();

But that’s not what we want. After spending hours with reading through the documentation (but not finding a single clue), I gave a shout to the official Propel Google user group and kindly received a response by François himself.

Guess what: Model Query classes extend Criteria, so you can use
addMultipleJoin() on a query object exactly the same way as you do it with
Criteria. The same goes for every Criteria method.

Cheers,
François

Of course, since Propel 1.4 there was a way to add complex / multiple join conditions within a Criteria object

$c = new Criteria();      
$c->addMultipleJoin(array(          
    array(BookPeer::AUTHOR_ID, AuthorPeer::ID),          
    array(AuthorPeer::NAME, 'Douglas Adams'))        
  Criteria::LEFT_JOIN);

and since a query object is a Criteria, both “syntaxes” can be mixed. But, sadly, that’s only half the truth when it comes to more complex queries. Consider the following situation: You’re writing a bulletin board software, where you have a users table and a postings table. When a user accesses a posting, it should be marked as read, but, obviously, it should be only marked as read for the specific user and not for everyone else. So we need a third table, called read_statuses where we will store a posting id, a user id and (maybe) a timestamp everytime a user accesses a new posting. What if we now wanted to display a list of unread postings for a certain user id? Our raw SQL query could look something like this:

SELECT p.*     
FROM postings p     
LEFT JOIN read_statuses rs ON rs.posting_id = p.id AND rs.user_id = 123    
WHERE rs.posting_id IS NULL

I tried the following approach first:

$q = PostingQuery::create()       
  ->addMultipleJoin(array(            
      array(ReadStatusPeer::POSTING_ID, PostingPeer::ID),           
      array(ReadStatusPeer::USER_ID, 123))          
    Criteria::LEFT_JOIN)       
  ->where('ReadStatus.PostingId IS NULL');

Although this query looks good, it won’t work because the Query object does not know about the ReadStatus alias used in the where condition. The downside in mixing both “syntaxes” is that you might be forced to stick with the old Criteria syntax even if you want to use the new API. Here’s the fix for the approach above

$q = PostingQuery::create()       
  ->addMultipleJoin(array(            
      array(ReadStatusPeer::POSTING_ID, PostingPeer::ID),           
      array(ReadStatusPeer::USER_ID, 123))          
    Criteria::LEFT_JOIN)       
  ->add(ReadStatusPeer::POSTING_ID);

It works fine but now it’s almost entirely solved with the old, verbose and hard to read Criteria API. It might not be a big deal for relatively simply queries like the one above, but, in my project, there were rather complex ones with lots of joins and where conditions. Eventually, I ended up writing almost pure Criteria code again. Bummer! So I got back to the “blackboard” and dug back into Propel’s and my code. And suddenly I found a solution that eventually made me a happy camper again. After adding a join condition to the query object, you can retrieve a corresponding join object from the query object and extend it for additional conditions. Here’s how:

$q = PostingQuery::create()       
  ->leftJoin('Posting.ReadStatus')       
  ->where('ReadStatus.PostingId IS NULL');           

$join = $q->getJoin('Posting.ReadStatus');   
$join->addCondition(ReadStatusPeer::USER_ID, 123, Criteria::LEFT_JOIN);

I really don’t know if François wanted those method calls to be exposed to the Propel users, cause it feels a little bit hacky, but it works fine for me, tidies up my code, makes it more readable and reduces the amount of Criteria code to an absolut minimum.

I hope this will help anyone seeking for a solution to a similar problem :)

Filed under  //   development   doctrine   orm   php   propel   symfony   tutorial  

How to embed a navigation controller inside a tab bar controller (Part 2)

There was a tremendous feedback for my original article about embedding a navigation controller inside a tab bar controller. Thank you guys :) I really appreciate that. I’m really sorry I’m a bit late with that but I had a ton of work in the last weeks. By far the most requests were about using a table view controller instead a normal subclassed view controller. I’ll show you a little update of my code where I’ve put a table view controller in the second tab that is displaying some dummy data. Actually it’s really pretty simple.

So fire up your Xcode IDE, load in “Tabs” project and let’s get started ;)

First of all, create a subclass of UITableViewController and name it “MyTableViewController”. After that create a property for the data that will be shown inside the table view. Here I chose a simple NSArray and named it “tableData”.

#import 


@interface MyTableViewController : UITableViewController {
  NSArray *tableData;
}

@property (nonatomic, retain) NSArray *tableData;

@end

Head over to the MyTableViewController.m file and change some of the methods that were generated for us. In order to create and initialize our tableData object with some dummy data, we’ll add the following line to the viewDidLoad: method

- (void)viewDidLoad {

  [super viewDidLoad];
  tableData = [[NSArray alloc] initWithObjects:@"foo", @"bar", @"baz", nil];
}

In tableView:numberOfRowsInSection: return the actual number of elements of tableData.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

  return [tableData count];
}

Finally, in tableView:cellForRowAtIndexPath: configure the fetched cell by setting cell.text to the corresponding object (that happens to be a NSString) of the tableData array

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

  static NSString *CellIdentifier = @"Cell";

  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
  }

  // Set up the cell...
  cell.text = [tableData objectAtIndex:indexPath.row];

  return cell;
}

After you have accomplished that, create a new XIB file called “ThirdView.xib”, open it in Interface Builder (IB) and simply drop a UITableView onto the empty view. Set File’s Owner to MyTableViewController and also make it the table views delegate and data source by control-dragging from the table view to File’s Owners and connecting the appropriate outlets.

Screen01

After you saved your ThirdView.xib, open your MainWindow.xib in IB. In the little window titled “MainWindow.xib” expand the Tab Bar Controller entry by clicking the little triangle left to it (if you don’t see it, make sure you chose the list style display) and also expand the My Navigation Controller entry that lives inside the Tab Bar Controller entry. Here you should see three entries: Navigation Bar, Second View Controller and Tab Bar Item.

Screen02

A UINavigationController holds a reference to a view controller that he actually takes care of. In this case it’s our SecondViewController, a simple subclass of UIViewController that manages a view that is prompting the words “Can I haz navigation?” (I assume you remember that from the original article). In order to have our navigation controller take care of our freshly created MyTableViewController, we have to change the Second View Controller entry. Select it, press +4 to head over to the identity inspector where you should see the Class entry set to “SecondViewController”. Change that to “MyTableViewController”.

Screen03

In addition to that, change the NIB Name in the attributes inspector (+1) from “SecondView” to “ThirdView” (to ensure that our MyTableViewController fires up our ThirdView.xib). If everything went well, the whole MainWindow.xib setup should look like this

Screen04

Well… that’s it ;) Save all your unsaved files, return to Xcode and choose build and run:

Screen05

I hope this little update answers most of your questions. If you still have problems or questions, please drop me a line. I’ll try to answer as quickly as possible ;)

Filed under  //   development   iphone   tutorial  

Customizing table view cells for fancy user interfaces

Table views are not only about displaying arbitary data in form of a list. Today, I will show you some little tweaks to create some nice user interfaces with the help of table views and their cells. I’ll present some tactics I’ve learned from the CocoaCast podcast made by Boris Klaydman. If you don’t know it yet, I recommend you to take a look. Those guys do an amazingly good job ;)

Some theory about table views

In order to use table views in your iPhone app, you take adventage of the UITableView class in combination with a subclass of UITableViewController. The controller helps you to bring the table view to life by acting as it’s data source. The table view will ask it’s data source, “What should I display at row xyz?”, and the data source will give him the answer. A UITableView consists of cells, instances of UITableViewCell or it’s subclasses. You’ll use the subclasses if you want to present cells with custom behaviour to the user.

Okay, enough theory – let’s fire up Xcode for some table view fun ;)

Here’s a picture of the complete user interface that we are about to build:

Screen01

Pretty neat, huh?

Start a new project

In Xcode click File > New Project, choose the Navigation-based Application as template for our project and name it “TableViewFun”. This will create a working application with a table view that lives inside a navigation controller. We won’t develop it from scratch this time since we want to concentrate about the customization of our table view cells. But if this is the first time for you working with UITableView I’ll briefly scan through the code and explain what has been done for us.

The most important part is the UITableViewController subclass that is named “RootViewController” in this template. When you open RootViewController.m, you’ll see a whole bunch of code that has been generated for us. The interesting part of that is the section marked as #pragma mark Table view methods. The following four methods are the data source methods that are used to set up our table and fill it with content:

numberOfSectionsInTableView:

Here we’ll tell the table view how many sections we want to display in our table.

tableView:numberOfRowsInSection:

This one will tell the table view how many rows live in each section. This can be some static value(s) or, considering you’ll want to show the contents of an NSArray in the table view, it could be the return value of a method call, e.g. [myArray count]

tableView:cellForRowAtIndexPath:

The actual table view cell for a given index path. An NSIndexPath is a C-struct that helps you navigate through a table view by indicating where you are, e.g. section 1, row 5. Because memory is limited on a mobile device like the iPhone, we’ll try to reuse cells in this method instead of always creating new ones. We will come to that in a minute ;)

tableView:didSelectRowAtIndexPath:

This will trigger some custom action for selecting a table view row. In most cases this would be pushing a new view controller onto the navigation stack for drilling down a table view, but we will use it for some other purpose.

There are some more (optional) data source methods defined in the UITableViewDataSourceProtocol but we won’t need them at this time. Feel free to browse the Apple docs if you want to know more about the remaining methods.

Some interface work

Open RootViewController.xib for some minor changes concerning the table view. It should open a view with an embedded table view. Click the table view to select it. In the Attributes section of the Inspector (+1) choose the “Grouped” style instead of the plain one. Save and return to Xcode.

Since this is all about customized cells, we’ll have to create them too. You could do this programatically, but I’ll present the way you would do it with the help of Interface Builder (IB). Control-click the Classes folder in “Groups & Files”, choose “New File” from the context menu and select “UITableViewCell subclass” from the “Cocoa Touch Class” section. Name that class TextFieldCell (this will be the cell holding a UITextField) and create the .h and .m files.

Now we’ll have to create the correspondig NIB (XIB) file. Right-click on the “Resources” folder, choose “New File”, create an empty XIB from the “User Interfaces” section of the iPhone OS group and name it “TextFieldCell.xib”. Open the file by double-clicking which will take you back to IB. Open the Library and drag a UITableViewCell onto the little window with both the “File’s Owner” and “First Responder” in it. Double click it and up pops another window which represents the table view cell. We’ll customize it by dragging a UITextLabel and a UITextField on it. Play around with their attributes until you have something like this:

Screen02

Set File’s Owner’s class to “RootViewController” and the UITableViewCell’s to “TextFieldCell”. In the Identity section (+4) for the table view cell create an outlet named textField for the UITextField instance you dropped onto our custom cell. Connect the outlet with the actual text field and define the outlet in TextFieldCell.h (e.g. by dragging it from IB to Xcode). Create and synthesize a corresponding property as well and don’t forget to release textField in the dealloc method. After that, create an outlet for our TextFieldCell named “textFieldCell” in RootViewController, connect it, create poperties, synthesize them and edit the RootViewController’s dealloc method.

TextfieldCell.h

#import 


@interface TextFieldCell : UITableViewCell {
 IBOutlet UITextField *textField;
}

@property (nonatomic, retain) IBOutlet UITextField *textField;

@end

RootViewController.h

#import "TextFieldCell.h"
#import "SwitchCell.h"


@interface RootViewController : UITableViewController {
 IBOutlet TextFieldCell *textFieldCell;
}

@property (nonatomic, retain) TextFieldCell *textFieldCell;

@end

After that we’ll create another custom cell, the one with an UISwitch in it. It’s pretty straight forward if you completed our first custom cell. So I assume you can do it on you own ;) Please name the UITableViewCell subclass “SwitchCell” and the corresponding XIB file “SwitchCell.xib”. The result should look like this

Screen03

SwitchCell.h

#import 


@interface SwitchCell : UITableViewCell {
 IBOutlet UISwitch *turnOnSwitch;
}

@property (nonatomic, retain) IBOutlet UISwitch *turnOnSwitch;

@end

RootViewController.h

#import "TextFieldCell.h"
#import "SwitchCell.h"


@interface RootViewController : UITableViewController {
 IBOutlet TextFieldCell *textFieldCell;
 IBOutlet SwitchCell *switchCell;
}

@property (nonatomic, retain) TextFieldCell *textFieldCell;
@property (nonatomic, retain) SwitchCell *switchCell;

@end

Adding the glue

Now that we have our customized cells it’s time to glue them to our table view. But first we have to do some more basic stuff. That is, to tell our table view the number of sections and rows for each section. For this example, we’ll have three sections (0, 1, 2). The first two sections (0, 1) will have one row each and the third section (2) will have three rows. The following snippet shows you how to do it:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

  switch (section) {
    case 0:
      return 1;
    case 1:
      return 1;
    case 2:
      return 3;
    default:
      // We'll never reach that case, but just to 
      // make sure that something will be returned
      return 0;
  }
}

Build an run our application and see what happens:

Screen04

Hmm, just what we wanted but it looks kinda naked, doesn’t it? What about adding some headers by implementing the tableView:titleForHeaderInSection: method:

- (NSInteger)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

  switch (section) {
    case 0:
      return @"Your name";
    case 1:
      return @"Turn me on";
    case 2:
      return @"Choose your destiny";
    default:
      // We'll never reach that case, but just to 
      // make sure that something will be returned
      return nil;
  }
}

Build and run… okay, that looks a lot better ;)

Screen05

But it’s quite obvious, that no of our custom cells were used. Instead, we’re presented the standard table view cells. In order to change that, we have to edit the tableView:cellForRowAtIndexPath: method. The default implementation is the following:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

 static NSString *CellIdentifier = @"Cell";

 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
 if (cell == nil) {
  cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
 }

 // Configure the cell.
 return cell;
}

As I mentioned before, it’s a good practice to reuse table cells instead of always creating new ones, cause this is always very expensive and it will create no good user experience. The default implementation uses a static identifier called “Cell” (or some other arbitary string) to reuse the table view cells. If no reusable cell could be dequeued, we have to create a new one. Here, an instance of the standard UITableViewCell class is used to create a new cell.

We have to decide which kind of cell has to be created for each section. In the first section of our table view we’ll need the TextFieldCell, in the second section we’ll need the SwitchCell and in the third and last section we will use the standard cells. We will load our two custom cells from their XIB files. In order to do that, we’ll call the loadNibNamed:owner:options: method from the mainBundle object:

[[NSBundle mainBundle] loadNibNamed:@"TextFieldCell" owner:self options:nil];

When doing like this our member variables textFieldCell and switchCell of the RootViewController class should be set to the right instances.

Take the snipped below for our implementation of tableView:cellForRowAtIndexPath: That’ll do exactly what we want.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

 UITableViewCell *cell;

 switch (indexPath.section) {
  case 0:
   cell = (TextFieldCell *)[tableView dequeueReusableCellWithIdentifier:@"TextFieldCell"];
   if (cell == nil) {
    [[NSBundle mainBundle] loadNibNamed:@"TextFieldCell" owner:self options:nil];
     cell = textFieldCell;
   }
   break;
  case 1:
   cell = (SwitchCell *)[tableView dequeueReusableCellWithIdentifier:@"SwitchCell"];
   if (cell == nil) {
    [[NSBundle mainBundle] loadNibNamed:@"SwitchCell" owner:self options:nil];
     cell = switchCell;
   }
   break;
  default:
   cell = [tableView dequeueReusableCellWithIdentifier:@"OtherCell"];
   if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"OtherCell"] autorelease];
     cell.accessoryType = UITableViewCellAccessoryNone;
   }
   break;
 }

 return cell;
}

Build and run and hopefully you’ll see the following:

Screen06

If your app crashes, check if you connected all your outlets properly. In most cases it has something to do with that ;)

So far, so good. Let’s take care of the remaining section. Here we want to implement a multiple selection view, kinda like the HTML checkboxes. First of all, create some selection items in the tableView:cellForRowAtIndexPath: method:

//...

 default:
  cell = [tableView dequeueReusableCellWithIdentifier:@"OtherCell"];
  if (cell == nil) {
   cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"OtherCell"] autorelease];
    cell.accessoryType = UITableViewCellAccessoryNone;
   switch (indexPath.row) {
    case 0:
     cell.text = @"Foo";
     break;
    case 1:
     cell.text = @"Bar";
     break;
    case 2:
     cell.text = @"Baz";
     break;
    default:
     break;
   }
  }
  break;

//...

Now, when the user taps one of these three cells, we want to represent a checkmark on the selected cell. That has to be done in the tableView:didSelectRowAtIndexPath: method:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

 // Retrieve the cell ...
 UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

 // For now we just take care of events
 // for the third section of the table view
 if (indexPath.section == 2) {
  if (cell.accessoryType == UITableViewCellAccessoryNone) {
   cell.accessoryType = UITableViewCellAccessoryCheckmark;
  }
  else {
   cell.accessoryType = UITableViewCellAccessoryNone;
  }
 }

 [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

Build, run and try “tap” one or more cells in the table view’s third section. You’ll notice a checkmark appearing at the right side, followed by a nice animation that indication that your selection has been completed.

Screen07

We’re done!

Uhm… actually we’re not really done yet ;) What about our text field? We had to implement the text fields delegate methods to complete it. Otherwise we could never dismiss the keyboard that appear when we tap onto the text field. But I’ll leave that as a little practice for you ;)

I hope you enjoyed my little tutorial about table view cells. I’m not quite sure if my way is to be considered as the best possible practice, but it works fine for me ;)

Filed under  //   development   iphone   tutorial  

How to embed a navigation controller inside a tab bar controller

It’s a commonly used design pattern in the iPhone world to combine tab bar and navigation controllers. In the official Apple developer docs you can find some clues how to solve this in a programmatically way, but you can only rarely find a good tutorial for doing this in Interface Builder (IB). So I’ll try to walk you trough all the steps for building a good application framework with one tab bar and one navigation controller.

If you like this tutorial, show some love and flattr it :)

Okay, let’s do this

First of all, launch your Xcode IDE, start a new iPhone window based project (we could use the tab bar based template but we want to create it from scratch) and call it “Tabs”. In “Groups & Files” expand the Resources folder and double-click MainWindow.xib to launch IB. Open the Library and drag a “Tab Bar Controller” onto the little Window named MainWindow.xib. The controller will appear on the workspace and a window with a tab bar layout will pop up.

Screen01

By the way: You can close the blank window layout, we won’t need it here ;) So far so good. We will now fill the two tabs that were created for us. But before that we need to create an outlet for the controller in the Tabs App Delegate object (that orange cube in the left window). We will name this outlet rootController and set the type to UITabBarController. Control-drag from the Tabs App Delegate to our Tab Bar Controller to connect the outlet.

Screen02

After that we return to Xcode in order to create this outlet in the TabsAppDelegate.h. Create a corresponding property as well (I assume you already know about Objective-C 2.0 Properties).

#import UIKit/UIKit.h

@interface TabsAppDelegate : NSObject 
{ 
  IBOutlet UITabBarController *rootController; 
  UIWindow *window; 
} 

@property (nonatomic, retain) IBOutlet UIWindow *window; 
@property (nonatomic, retain) IBOutlet UITabBarController *rootController; 

@end

Now, we will set and create two different controllers for the tabs: First, a subclass of UIViewController and secondly our long-awaited navigation controller ;) Head back to Xcode, control-click the Tabs project (the very first icon at the “Groups & Files” view) and choose Add > New File … Select an UIViewController subclass from the “Cocoa Touch Classes” section and name it FirstViewController (do not forget to create the .h file too). In addition to that create a FirstView.xib file in the Resources folder, open it in interface builder and build a simple view like the one below.

Screen03

In the Identity Inspector (+4) set the class of “File’s Owner” to FirstViewController and control-drag from “File’s Owner” to “View” to connect the view outlet. Save your work an switch back to MainWindow.xib. Now we have to tell the first tab that it has to care about our FirstViewController. In order to do so, select the first tab, open the Identity Inspector (+4) and set the tab’s class to FirstViewController. But what about it’s content? Switch to Attributes Inspector (+1) and choose FirstView from the NIB Name’s drop-drop down box. – What have we done here? We told the first tab to present the contents of our FirstView.xib. Finally, change the tab bar item’s title to “Simple View Controller”. If everything went well, the layout should look like this

Screen04

Time for our first launch, isn’t it? Almost. But first, we have to make some changes in our TabsAppDelegate.m

#import "TabsAppDelegate.h" 

@implementation TabsAppDelegate 

@synthesize window; 
@synthesize rootController; 

- (void)applicationDidFinishLaunching:(UIApplication *)application 
{ 
  // Override point for customization after application launch 
  [window addSubview:rootController.view]; 
  [window makeKeyAndVisible]; 
} 

- (void)dealloc 
{ 
  [window release]; 
  [rootController release]; 
  [super dealloc]; 
} 

@end

In [window addSubview:rootController.view] we add the view property of our root controller as subview to the window. Without that, our tab bar would never be seen on our device respectively our simulator. Save, build and run.

…drumroll…

Screen05

Woohoo! Everything went perfectly fine :) (If it did not for you, check your code and all outlets' connections). But now for the part you all waited for. Generally it’s almost the same steps but we have to watch out for some details. Create a new UIViewController subclass and name it “MyNavigationController” (don’t forget the .h file). Since we need an UINavigationController instead of an UIViewController, open MyNavigationController.h and change UIViewController to UINavigationController in the @interface directive.

#import UIKit/UIKit.h

@interface MyNavigationController : UINavigationController 
{ 

} 

@end

Since we will have to deal with the navigation controller programmatically to push and pop views, it’s a good idea to create an instance variable for it in our Tabs App Delegate class.

#import UIKit/UIKit.h
#import "MyNavigationController.h" 

@interface TabsAppDelegate : NSObject 
{ 
  IBOutlet UITabBarController *rootController; 
  MyNavigationController *navigationController; 
  UIWindow *window; 
} 

@property (nonatomic, retain) IBOutlet UIWindow *window; 
@property (nonatomic, retain) IBOutlet UITabBarController *rootController; 
@property (nonatomic, retain) MyNavigationController *navigationController; 

@end

Our navigation controller needs at least one view controller to display its contents. So go ahead, create another UIViewController named “SecondViewController” and also create a “SecondView.xib” with a layout similar to the first one.

Screen06

Save it and switch back to MainWindow.xib. Here comes the tricky part: Similar to the FirstViewController, select the second tab and set its class to MyNavigationController. After that, select the Tab Bar Controller and open the Attributes Inspector (+1). In the “View Controllers” section, change the term “Item 2” to “Navigation View Controller” (this will be displayed as the second tab’s title) and switch the class to NavigationController.

Screen07

Now we set the navigation controller’s root view controller: In the little MainWindow.xib Window expand the Tab Bar Controller. You should now see a Tab Bar icon and two sub controllers. Select the second one and expand it, too… Aaaaah, there it is! The navigation controller’s root view controller. Select it, set its class (+4) to SecondViewController and choose (+1) SecondView as NIB Name for the content. If everything went well, your layout should look like this:

Screen08

That’s it! Build, run and enjoy your creation

Screen09

As you can see, we built a sophisticated fully-fledged layout with less than 10 lines of self-written code. Feel free to play around with it or try to add some some more view controllers to the tab bar layout (it’s as simple as drag and drop).

Update

Due to huge demand I’ve added a second part of this tutorial that’ll walk you through the steps of having a table view controller instead of a normal view controller as second tab. Jump right into part 2

Filed under  //   development   iphone   tutorial  

About

Addicted to shiny apples, beautiful pixels, awesome code and electronic music.

TwitterFacebookFlickr