Tag Archives: ios

Master Detail App – with or without Storyboard

I’ve created two projects to demonstrate the navigation techniques I described in my previous post. If I am creating a new project and I do not have to support previous versions of iOS, then I would definitely use storyboarding. It makes navigation so much easier and consistent. If you have not come across it, I would recommend iPad and iPhone Application Development by Paul Hegarty from Stanford University in iTuneU. It is interesting, informative, educational and most importantly free. 🙂

Back to the projects. The main things I was trying to demonstrate in those projects were navigation techniques. So some of the things in those projects may not pass the quality check for production code. Please turn a blind eye on those things and I hope there is something useful for you in those projects. 

The projects demonstrate (one using storyboard and one not):

  1. loading a new master page from the current master page
  2. loading a new detail page from the current master page
  3. loading a new detail page form the current detail page

The trigger for loading a new detail page from the current detail page is a single tap (forward) and two finger tap (backward). More detailed description of how it works is available in the previous post. 

Here are the links to the projects on github.

 

Tagged , , , ,

Fitting in with Master-Detail Application in XCode 4.3

As a Windows developer coming to terms with iOS development in XCode, this will log my struggles in getting the basic structure of Master-Detail application for iPad  in XCode 4.3 with multiple master views and multiple detail views. As I was looking for answers, my googlefu can only find me mostly those for older versions of XCode. For example, there is no more RootViewController and no MainWindow.xib. And there are a few other details changed which made things difficult for a noob like me to follow and understand.  Let’s talk about this new structure in XCode 4.3.

I would assume you know what a Master-Detail application look like from a user perspective. I think Mail app would be be a good exmaple of its kind.

Master, Detail and the Split View Controller

At the heart of a Master-Detail application is a split view controller. When we create a new project of type Master-Detail application the following function is the generated in AppDelegate.m file.

 1: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 2: {
 3:     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 4:     // Override point for customization after application launch.
 5:
 6:     MasterViewController *masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController" bundle:nil];
 7:     UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController];
 8:
 9:     DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil];
 10:     UINavigationController *detailNavigationController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
 11:
 12:     masterViewController.detailViewController = detailViewController;
 13:
 14:     self.splitViewController = [[UISplitViewController alloc] init];
 15:     self.splitViewController.delegate = detailViewController;
 16:     self.splitViewController.viewControllers = [NSArray arrayWithObjects:masterNavigationController, detailNavigationController, nil];
 17:     self.window.rootViewController = self.splitViewController;
 18:     [self.window makeKeyAndVisible];
 19:     return YES;
 20: }

A view controller each for master and detail views are created namely, masterViewController and detailViewController. Those controllers are added to corresponding navigation controllers. Those navigation controllers are again added into viewControllers array of splitViewController. It is important to remember that it is the navigation controllers not master and detail view controllers themselves which are added to the split view controller here.Finally splitViewController is assigned to window.rootViewController property.

The other thing to note is detailViewController is also assigned to masterViewController’s detailViewController property which can be useful if your application has only one master. And also detailViewController implements UISplitViewControllerDelegate which handles showing or hiding of popover button on the navigation bar of the detail view as the user changes the device orientation from portrait to landscape and vice versa.

Now, I would like to switch master views as the user select items in it. That’s relatively easy. I just create more UIViewControllers and push them onto master view navigation stack like so in the event handler, assuming you use UITableView for displaying the items.

 1: - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 2: {
 3:     ....
 4:     [[self navigationController] pushViewController:secondMainController animated:YES];
 5: }

That way, you can keep drilling down into multiple level of master views. In this case, I do not want to keep passing the detailViewController as I change master views. I think it is just unwieldy and I might forget to pass it on at one point. Now assume, I have reach the item I want in the master view, I would like to bring up the corresponding detail view. I was stuck there for a bit as I could not figure out how to tell detail view to switch to this new view I want.

Devil in the detail

Then I notice there is splitViewController property in all UIViewController objects. It is in master, sub master, detail everywhere. UIViewController class reference says
“If the receiver or one of its ancestors is a child of a split view controller, this property contains the owning split view controller.”. My thought at the point was, if I could get to split view controller, which has an viewControllers array propery, I could get to detail view.

According to the above function in AppDelegate.m, controller for master view is at index 0 and detail view is at index 1. So I got my detail view controller. So I thought and did something like:

 1: [[detailViewController navigationController]
 2:     pushViewController:myNewDetailViewController animated:YES];

The code above fails to bring in the view I like, My main concern at that point was not getting the correct split view controller and not getting the correct viewControllers property. But after a few debug rounds, I confirmed I was getting the correct split view controller and viewControllers property with 2 controllers in it. That was because. as you may notice, controllers added to viewControllers array in splitViewController are navigation controllers, not view controllers. So there is no need to call [self navigationController] again. So the code becomes,

 1: UINavigationController *detailNavigationController =
 2:     [[self splitViewController] viewControllers] objectAtIndex:1];
 3: [detailNavigationController
 4:     pushViewController:myNewDetailViewController animated:YES];

Don’t push my buttons

At that point, I got the detail view I want showing up when I select an item in master view. However, there is one problem. I do not want the back button added to the navigation bar of my new detail view by the navigation controller. It is just standard behaviour of the navigation controller to provide back button when a new view is pushed onto the stack. I need the popover button to be displayed instead of back button. I do not need back navigation in the detail view anyway.

The first thing I need to do was to not push view into navigation stack but replace the existing view. Here is how I have it, with ideas I of course, got from applying my googlefu .

 1: UINavigationController *navController=[[UINavigationController alloc] init];
 2: [navController pushViewController:newDetailViewController animated:YES];
 3:
 4: NSArray *viewControllers = [[NSArray alloc] initWithObjects:
 5:                                 [[[self splitViewController]viewControllers] objectAtIndex:0],
 6:                                 navController,
 7:                                 nil];
 8:
 9: [[self splitViewController] setViewControllers:viewControllers];

The idea is to replace the whole navigation controller not just the stack inside it. I created a new navigation controller with the view I want at the top. And create a new array of view controllers with master navigation controller at index 0 and new navigation controller at index 1. And assign the array to viewControllers property of splitViewController. Then I have my new detail view displayed with the back button on navigation bar.

So my next quest is how to get ahold of that popover button to be added to the new detail view.

Sanity in the end

I got that sorted too and I put all those functionalities required to swap detail view into a category extending the UISplitViewController as followed:

 1: #UISplitViewController+RightNavigationController.h
 2: @interface UISplitViewController (RightNavigationController)
 3:
 4: -(void)changeDetailViewWithController:(UIViewController<UISplitViewControllerDelegate>*) viewController;
 5:
 6: @end
 7:
 8:
 9:
 10: #UISplitViewController+RightNavigationController.m
 11: #import "UISplitViewController+RightNavigationController.h"
 12:
 13: @implementation UISplitViewController (RightNavigationController)
 14:
 15: -(UIBarButtonItem*)popPopoverButton{
 16:     UINavigationController *detailNavigationController = [[self viewControllers] objectAtIndex:1];
 17:     UIViewController *detailViewController = [[detailNavigationController viewControllers] objectAtIndex:0];
 18:     UIBarButtonItem *popoverButton = [[detailViewController navigationItem] leftBarButtonItem];
 19:     [[detailViewController navigationItem] setLeftBarButtonItem:nil];
 20:     return popoverButton;
 21: }
 22:
 23: -(void)changeDetailViewWithController:(UIViewController<UISplitViewControllerDelegate>*) viewController{
 24:
 25:     UIBarButtonItem *popoverButton = [self popPopoverButton];
 26:
 27:     UINavigationController *navController=[[UINavigationController alloc] init];
 28:     [navController pushViewController:viewController animated:YES];
 29:
 30:     NSArray *viewControllers = [[NSArray alloc] initWithObjects:[[self viewControllers] objectAtIndex:0], navController, nil];
 31:
 32:     [self setViewControllers:viewControllers];
 33:     [self setDelegate:viewController];
 34:
 35:
 36:     [[viewController navigationItem] setLeftBarButtonItem:popoverButton];
 37: }
 38:
 39: @end

Do not forget to implement UISplitViewControllerDelegate in the header files of the new detail views you are creating. The two function  required to be implemented for that delegate  can be pretty much copied from the default detail view implementation. If you find your new detail view does not have “masterPopoverController” which is used in the copied functions, you could delete those lines requiring “masterPopoverController”. If you do not implement “UISplitViewControllerDelegate”, you will lose the popover button when switching device orientation back and forth.

There are assumptions made which might not work for you. It assumes view controller at index 0 of viewControllers property in splitViewController is main navigation controller and index 1 is detail navigation controller. the view controller at index 0 of viewControllers array in detail navigation controller is the detail view controller and there is only one view. The left bar button item on nvaigation bar of detail view controller is the popover button. If these assumptions work for you, this solution might not be too bad.

Links to demo projects are available in next post.

Tagged , , ,