Search
Rich's Mad Rants
Powered by Squarespace
« FMSAlertManager added to GitHub | Main | Notes about Xcode 4.5 »
Monday
Sep242012

Advanced Storyboard Techniques

I'm not afraid to admit it. I love Storyboards. They were one of the new features in iOS 5 that greatly changed how I develop applications. I must say, it's nice to see how they continue to evolve and mature in iOS 6.

Not surprisingly, I heavily emphasized Storyboards in my book, Creating iOS 5 Apps: Develop and Design. However, it turns out they are a lot more flexible and powerful than I originally gave them credit for. So, without further ado, here's a shotgun blast of advanced Storyboard tips and techniques. Some of it applies to iOS 5; however, much of it is new for iOS 6. 

Segue Timing

Understanding the timing behind segues is important to using them successfully. When a new segue fires, the following steps occur:

  1. The destination view controller is instantiated (initWithCoder: is called)
  2. The source view controller's prepareForSegue:sender: method is called
  3. The destination view is loaded
    1. The destination's viewDidLoad is called
  4. The destination view appears
    1. The source's viewWillDisappear: is called
    2. The destination's viewWillAppear: is called
    3. The source's viewDidDisappear: is called
    4. The destination's viewDidAppear: is called.

When a view controller is disposed (dismissing a modal view or popping a navigation view), we go through the following sequence:

  1. The source's viewWillDisappear: is called
    1. The destination's viewWillDisappear: is called
    2. The source's viewWillAppear: is called
    3. The destination's viewDidDisappear: is called
    4. The source's viewDidAppear: is called.
  2. The destination view is deallocated

There are a few key point worth noting. Every time we fire off a segue, we will instantiate (and then later deallocate) a new copy of the destination's view controller. If you need to preload or cache your view controllers, you will want to manage the view controllers programmatically.

Also, when the prepareForSegue:sender: method is called, the destination controller's view has not yet loaded. You cannot access any of that controller's outlets. The best practice is to just pass whatever data you wish to the destination controller--storing the data in properties as necessary, and then let the destination use its properties in its viewDidLoad method to configure its own outlets.

Finally, our code can alter the order--sometimes in unexpected ways. For example, if we touch the destination's view property before its view has loaded, we will force the view to load. This can cause the destination controller's viewDidLoad method to fire earlier than we expected. Just another reason to leave the view (and everything inside the view) alone. 

Loading New Views Programmatically

Let's say we want to draw a segue to a view controller that builds its views programatically (by overriding its loadView method). Originally, I thought this was not possible; that we would have to manage the transition to this view controller entirely in code. Turns out, we can have the Storyboard automatically instantiate and deallocate our view controller--even though we're not designing its view in the Storyboard. There's just one small trick to make it all work:

  1. Make a UIViewController subclass.
  2. Implement its loadView method.
  3. In the Storyboard, drag out a view controller as normal.
  4. Set the view controller's class to your subclass.
  5. Set the segue to new view controller.
  6. Now, comes the secret sauce--delete the view from the view controller's scene.

ProgramaticViewController

When the segue fires, the view will be created using the slightly modified sequence:

  1. The destination view controller is instantiated (initWithCoder: is called)
  2. The source view controller's prepareForSegue:sender: method is called
  3. The destination view is loaded
    1. The destination's custom loadView method is called
    2. The destination's viewDidLoad method is called
  4. The destination view appears
    1. The source's viewWillDisappear: is called
    2. The destination's viewWillAppear: is called
    3. The source's viewDidDisappear: is called
    4. The destination's viewDidAppear: is called.

We can then instantiate and layout our views and subviews in the loadView method, as normal. We can even draw segues from this view controller to other view controllers in the Storyboard--though, since we don't have any controls to link the segue to, we will need to trigger these segues in code (draw the segue from the controller itself, then have the controller call performSegueWithIdentifier:sender: to trigger the desired segue).

Loading New Views Using Nibs

Being able to include programatically-designed view controllers is cool--but it would be even better if we could load the view controller from another nib file. This is basically the same as the previous example, except instead of implementing the loadView method, we provide a properly named nib file. 

Note: There's no way to specify the nib file for our view controller. However, whenever no name is provided, UIViewController searches for a nib whose name matches the view controller's class name. So, if we have a view controller called MyCoolViewController, it would look for MyCoolViewController.xib. As long as the names match, the nib will load as expected.

Loading New Views From Other Storyboards

For me, this is the holy grail for Storyboards. I'd love to be able to create a Storyboard that is a composite of other Storyboards. Ironically, there doesn't appear to be any way to do this from within Interface Builder. The "add the view controller, then delete the view" trick won't work. Don't get me wrong, we can still string together Storyboards, but we have to stitch everything together in code.

To segue to another Storyboard:

  1. Get the Storyboard by calling UIStoryboard's storyboardWithName:bundle: method.
  2. Get the view controller by calling instantiateInitialViewController (to get the initial view controller) or instantiateViewControllerWithIdentifier: (to get an arbitrary controller from within the Storyboard).
  3. Present the view controller (e.g. presenting it modally or pushing it onto a navigation controller).

It's a bit annoying that we have to do all this in code--but it's still a nice trick to master. There are often good reasons to split a Storyboard into separate, independent modules. For example, you might have different people working on different parts of the UI. Or, you might just want to prevent a single Storyboard from becoming overly complex.

On the other hand, many of the benefits of using Storyboards (e.g. drawing segues or unwinding segues between scenes) won't work between Storyboards. So, there are also good reasons to keep everything together. I guess the bottom line is, don't split things up just for the sake of splitting things up. But, don't be afraid to split them up either.

Unwinding Segues

In iOS 5, we could easily pass data forward along the segues--but we didn't have an automated way of passing data back. To get around this, we typically used a delegate pattern. The destination view would declare a delegate protocol. The sending view would then implement this protocol. In the prepareforSegue:sender: method, the sender would assign itself as the destination's delegate. The destination controller could then call any of the delegate methods--allowing us to send arbitrary messages back upstream.

While this is still a powerful design pattern, iOS 6 has partially automated the procedure--letting us more easily create a wider range transitions. Basically, we can define how the Storyboard will unwind any given segue.

In the controller you wish to return to, implement a method that returns an IBAction, and that takes a single UIStoryboardSegue argument:

- (IBAction)returnActionForSegue:(UIStoryboardSegue *)returnSegue {

// do useful actions here.

}

Now, in the scene we wish to unwind, control-drag from the UI element that will trigger the return to the scene's exit icon (the green icon in the dock). In the popup menu, select the previously defined return item.

DragToExitIcon

Note1: We cannot simply create a segue from the button back to the desired scene, since that will instantiate an entire new copy of the scene. Instead, we want to return to the existing copy of the scene.

Note2: We don't necessarily need to return to the previous scene. This is particularly handy if we are pushing a number of scenes into a navigation controller, and want to pop back to an arbitrary point in the middle--or if we're mixing modal and push segues, and want to unwind to any point in the chain.

The Details

The system will search for the return destination by calling canPerformUnwindSegueAction:fromViewController:withSender: on the current view controller. By default, this method simply checks to see if you have a method whose selector matches the return action. If it does not find a match, it will search the parent view controller, and will continue searching up the chain of parent view controllers until it finds a match.

We can override canPerformUnwindSegueAction:fromViewController:withSender: to add more control to the search--for example, if we define the same return action in multiple view controllers, and then override this method to make sure we return to the correct one.

Furthermore, container view controllers are responsible for searching their child view controllers. Apple's built-in container views all do the right thing. For example, a UINavigationViewController will search through the view controller stack from the top down, starting just below the exiting controller.

If we want our own custom container controller to search through its child view controllers, we need to override viewControllerForUnwindSegueAction:fromViewController:withSender:. In this method, we should iterate over our child view containers, calling canPerformUnwind… on them. If we find a match, we should return it. If not, we should call the superclass's implementation and return the result.

Once it finds a match, the system then calls segueForUnwindingToViewController:fromViewController:identifier: on the destination controller. Again, we need to override this method for our custom container controllers, returning a segue that performs the animation and other necessary steps to unwind the view controllers.

Then, the system calls prepareForSegue:sender: on the exiting view controller, followed by calling the exit action on the destination view controller. We can use either of these steps to pass data back to the return point.  Finally the system triggers the segue.

This is a bit complicated, so let's walk through a concrete example. Let's say I have a multi-page form that I want users to fill out. To use the form, I modally present a navigation controller. The navigation controller's root view controller is the first page of my form. As the user fills out each page, I push a new one on the navigation controller. On the final page I have a done and a clear buttons. 

The done button should take the user back to my original view (the navigation controller's presenting view controller). The clear button should just take them back to the beginning of the form.

To set this up, I need to implement two methods. formCompleted: should be implemented on the view controller that will present the form, while clearForm:  should be implemented on the first page's view controller. I then draw connections from the done button to the formCompleted:  exit action, and from the clear button to the clearForm:  exit action.

When the clear button is pressed, the system searches the final page's view controller for the clearForm:  method. It doesn't find a match, so it searches the parent view controller--our navigation controller. The navigation view controller manages the search of its children. Basically, it starts at the top of the stack, just below the last page controller, and works its way down.  Eventually it finds the method on its root view controller--so it returns that controller as the destination.

The navigation controller then creates a segue where it pops to the destination with animation, and passes this back to the final page controller. The final page's prepareForSegue:sender: is called. The root controller's clearForm: method is called. Then the segue is triggered.

If I press the done button, it will search the last page, then search the navigation controller. This time, the navigation controller doesn't find anything, so the system will search its presenting controller. Here a match is found. The presenting controller automatically creates a segue to dismiss the modal view controller with the correct animation. Again, the final page's prepareForSegue:sender: is called. The root controller's formComplete:  method is called, and the segue is triggered.

As you can see, the ability to unwind segues allows us to easily return to any existing view controller in our view controller hierarchy. in iOS 5, these transitions required a considerable amount of custom code--the more complex the transition, the more complicated the code. Now, these transitions can largely be automated, saving us time and effort, and hopefully avoiding bugs.

Container Views

The iOS SDK has always had two different types of view controllers. The most basic type are the content view controllers. These are the view controllers that we create to display our content. However, the system also had a number of pre-build container view controllers: UINavigationControllerUITabBarController and others. These controllers usually provided a minimal user interface of their own (the navigation or tab bar itself). Instead, they manage a number of content view controllers for us.

With iOS 5, Apple gave us the ability to create our own content view controllers. This is a surprisingly powerful technique, letting us easily break a complex user interface into several independent parts. However, all of the management of these sub view controllers and their views had to be done in code.

Now, with iOS 6, we can add a container view to our Storyboards (though, not our nibs). Simply drag a Container View object from the library and place it on one of your scenes. Interface Builder will automatically add a content view controller for the container view, with an embed segue--though you can change the segue to embed any view controller in the Storyboard.

AddingAContainerView

When the parent view controller is loaded, this segue will fire, and the child view will also load. Timing-wise, the child view will finish loading before the parent's viewDidLoad method is called, so you can safely configure the child from within the parent's viewDidLoad.

Unfortunately, this only gets us about half way to our goal. Don't get me wrong. It's a cool new feature, and I love the fact that I can graphically lay out my container views. However, I typically use container views because I'm going to be dynamically swapping the content within those views. And this may or may not be possible to set up in the Storyboard.

If the child views are responsible for swapping themselves around, then you can simply draw segues from one child to the next as usual. There are no problems, and everything just works (though, see the note on unwinding segues above).

However, there's no way to have the parent view controller drive these segues. To really let the parent freely drive arbitrary transitions between all the possible child controllers, you'll have to manage everything in code. Here's a common work flow:

  1. Instantiate the new child view controller.
  2. Set the frame of the new child controller's view to the existing container view.
  3. Animate the transition between the views.
  4. Remove the old child controller from the parent.
  5. Add the new child controller to the parent.

Assuming you only have a single container view--and thus only a single child controller--you could implement this as shown below. Obviously, if you want to preload or cache the view controllers, or if you have multiple container views, your implementation may become much more complex. 

// Assumes we have a MyChildViewController.xib, or that 
// MyChildViewController programmatically creates its views.
MyChildViewController *controller = [[MyChildViewController alloc] init];

// Assumes we only have 1 child view controller
UIViewController *currentChild = [self.childViewControllers lastObject];
controller.view.frame = self.containerView.frame;

[UIView
 transitionFromView:self.containerView
 toView:controller.view
 duration:0.25f
 options:UIViewAnimationOptionTransitionCrossDissolve
 completion:^(BOOL finished) {
    [currentChild removeFromParentViewController];
    [self addChildViewController:controller];
}];

 

If you want to load another controller from within the Storyboard, you would modify the code as shown below:


UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];

// Must match the controller's Storyboard ID
SecondChildViewController *controller =
[storyboard instantiateViewControllerWithIdentifier:@"SecondChild"];


// Assumes we only have 1 child view controller
UIViewController *currentChild = [self.childViewControllers lastObject];
controller.view.frame = self.containerView.frame;

[UIView
 transitionFromView:self.containerView
 toView:controller.view
 duration:0.25f
 options:UIViewAnimationOptionTransitionCrossDissolve
 completion:^(BOOL finished) {
    [currentChild removeFromParentViewController];
    [self addChildViewController:controller];
}];

 

Be sure to set the matching Storyboard ID in the Identity Inspector for the child view controller--otherwise this won't work.

And that's it for now. I hope you found at least some useful information in here. Also, please let me know if you find any other cool Storyboard tricks.

References (4)

References allow you to track sources for this article, as well as articles that were written in response to this article.
  • Response
    Response: Wayfair Promo Code
    Freelance Mad Science Labs - Blog - Advanced Storyboard Techniques
  • Response
    Response: Wayfair Coupon
    Freelance Mad Science Labs - Blog - Advanced Storyboard Techniques
  • Response
    Response: Wayfair Coupons
    Freelance Mad Science Labs - Blog - Advanced Storyboard Techniques
  • Response
    Freelance Mad Science Labs - Blog - Advanced Storyboard Techniques

Reader Comments (22)

Nice post. I like the way you start and then conclude your thoughts. Thanks for this information .I really appreciate your work, keep it up

You mention implementing -loadView but you don't even say how. Everything you mention about loading it programatically is as vague as what the speaker in the WWDC mentioned--"implement loadview" What exactly are we supposed to implement?

December 24, 2012 | Unregistered CommenterMartin

Martin,

It's conceptually very simple. In your UIViewController subclass, override the loadView method.

There are only two rules:

1) Do NOT call [super loadView].
2) At some point in your custom loadView method, you must create your root view and assign it to self.view.

That's it. You can create a single view. You can create an arbitrarily complex view hierarchy. It's all up to you.

Having said that, I strongly recommend only programmatically setting up views for the simplest cases. If you have more than a couple of views, I'd adviser switching to Interface Builder. In my own code, I typically use Interface Builder for all my view controllers--even if they only have a single, root view.

I hope that helps,

-Rich-

December 24, 2012 | Registered CommenterRichard Warren

This is a great post! Huge amount of goodness. I'll go ahead and read your other stuff immediately.

However, I tried the container view example at the end of the article and it seems to be miswritten. At least it never worked reliably for me. I got it working in the following way though.


- (void)switchViewController:(UIViewController*)newViewController {
newViewController.view.frame = self.containerView.frame;
[self addChildViewController:newViewController];

UIViewController *currentChild = [self.childViewControllers lastObject];

[self
transitionFromViewController:currentChild
toViewController:newViewController
duration:0.25f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:nil
completion:^(BOOL finished) {
[currentChild removeFromParentViewController];
}];
}

Note that I have used transitionFromViewController and not transitionFromView. The other stuff with the storyboard loading just works like charm.

Also, it would be good to mention or change the screenshot to show that containerView is an outlet on the view controller for the ContainerView control (which is a UIView) actually.

Thanks a lot,
Andras

January 4, 2013 | Unregistered CommenterAndras

Sorry, I messed up my configuration and forgot to set the containerView outlet when the code in the post was not working. Now everything is perfect. The code in my previous comment is an alternative.
Regards,
Andras

January 4, 2013 | Unregistered CommenterAndras

Thanks for the feedback Andras. Missed outlets can be a real pain, since sending a message to nil doesn't create any obvious errors--it just silently does nothing. Glad you figured it out and provided an alternate code as well.

-Rich-

January 16, 2013 | Registered CommenterRichard Warren

Thanks for writing this Andras - it's most helpful in that it explains what is going on.

I plan to develop using storyboards from now on. As I wind down my current project, I'm wondering if it's a good idea to add a tableview storyboard, leaving it with all the other NIBs? It's so much easier to craft a nice looking custom tableview using a storyboard.

So, is this even possible? can I use the trick where the storyboard name matches the VC name? or do something else.

Thanks

January 19, 2013 | Unregistered CommenterDavid

Question to Rich:
I love storyboards too. However, in version controlled, distributed projects it easily becomes painful to merge the XML contents. (Same applies to the project file but that's a different story). Do you have an practices that would make storyboards more convenient in these cases?

Regards,
Andras

January 19, 2013 | Unregistered CommenterAndras

David,

You can create a storyboard that just has the table view controller. Unfortunately, you'll need to load the storyboard programatically. You can't just use the same name for the storyboard and table view controller subclass. See LOADING NEW VIEWS FROM OTHER STORYBOARDS for the instructions.

Andras,

I'd strongly recommend only letting one developer modify a storyboard file at a time. Trying to deal with conflicts is just too much of a pain. However, you can break the project up into multiple storyboards, and give each developer their own piece to develop/maintain.

Note that this doesn't just apply to storyboards. The same advice also applies to NIB files--though you're much less likely to have conflicts in an individual nib file, since they're a smaller, more contained module.

I hope that helps,

-Rich-

January 19, 2013 | Unregistered CommenterRich

Thanks Rich. Sorry for wrong attribution before.
I'll look for the piece you wrote..

January 19, 2013 | Unregistered CommenterDavid

David,

Just to be clear, "LOADING NEW VIEWS FROM OTHER STORYBOARDS" isn't another piece. It's a section in this article. That should have all the information you need to load a view controller from a storyboard programatically. Let me know if you have any questions.

-Rich-

January 20, 2013 | Registered CommenterRichard Warren

Hi Rich,

Thank you for such an informational post!

I have a MainViewController (Root Controller) which has 5 UIButtons on the left (B1 - B5). In the middle of the Main View Controller, I have a ContainerView. This ContainerView has 5 scenes (one of which is navigation controller) that I would like to wire up to the 5 UIButtons. When buttons are pressed, I'd like the scenes to open to up inside the container.

Here is an image of what I'm trying to do:
http://stackoverflow.com/posts/14493893/revisions

I read your post and applied your code:
// Assumes we have a MyChildViewController.xib, or that
// MyChildViewController programmatically creates its views.
MyChildViewController *controller = [[MyChildViewController alloc] init];

// Assumes we only have 1 child view controller
UIViewController *currentChild = [self.childViewControllers lastObject];
controller.view.frame = self.containerView.frame;

[UIView
transitionFromView:self.containerView
toView:controller.view
duration:0.25f
options:UIViewAnimationOptionTransitionCrossDissolve
completion:^(BOOL finished) {
[currentChild removeFromParentViewController];
[self addChildViewController:controller];
}];

To make the corners round, I have a UIView. On top of the UIView I have a container. After applying your code, I ran the app. I see the animation, but it appears that the container is removed as I'm left with the UIView inside the Root Controller. To test, I changed the UIView's background color to black and left the container scene's background color to white. After I clicked on the button, I was left with a black UIView inside the UIViewController.

Seems like your post has the most of amount of information on the web about ContainerView so I thought I'd ask. Thank you in advance.
-Ahmed

January 24, 2013 | Unregistered CommenterAhmedS

Ahmed,

I haven't been using the container views regularly in projects, since my current projects all must support devices back to iOS 5, and it's been a while since I wrote this. So, I'll have to go back and look at my code and refresh my memory. But I wanted to give you a quick reply.

I'm also a little bit confused by your description. When you say you set the UIView's background to black, which view are you talking about.

Regardless, it sounds like you have a very complicated setup here. Can you try simplifying it and see if you can get that working, then you can throw in all the bells and whistles. For example, get rid of the rounded corners and see if it works.

Also, if I understand you correctly, it sounds like the view appears during the animation, but vanishes shortly afterwards. If that's the case, it sounds like it's accidentally getting deallocated. This could either be because you're removing the wrong view, or because ARC is overly optimizing something. It's hard to tell, without actually looking at the code.

Let me look at my old code again, and check out a few other ideas Other than that, try to make it as simple as possible when first starting. You may even want to start with a simple, sample app, just to get the pattern down.

-Rich-

January 24, 2013 | Registered CommenterRichard Warren

Rich,

Thank you for your swift reply.

So I did remove the UIView for rounded corners and I still can't seem to isolate the problem. I'll try to cliffy:

I have a UIViewController. Inside this UIViewController I have a ContainerView at (x=68 Y=6 Width: 244 Height: 360). This ContainerView has 5 scenes that work fine as I can click Next and Back to get to one through the fifth scene and back. Imagine a User form that's divided into 5 scenes inside a containerView. User fills one scene out and clicks Next to get to the next scene or Back to get back to the previous scene. This seems to be working as it should.

However, also in this same UIViewController, I have 5 buttons. Each of these 5 buttons need to point to one of the scenes. B1 (ButtonOne) will point to Scene 1; B2 (ButtonTwo) will point to Scene 2 and so forth.

What I'm trying to accomplish:
On the initial screen of the ContainerView the User can click the next button 4 times to get the 4th Scene inside the ContainerView (this is working fine); or can click on B4 (button number 4) to get the 4th scene (this is what I need help with). If a cached scene 4 exists, the user will jump to the cached scene, otherwise a new instance of scene 4 will be created. Similarly, to get get back to the first scene, the user can either click the Back button inside the ContainerView Scenes 4 times; or click on B1 (button number 1) to get back to the first scene (cached, if available). I would like to call the cache'd scene's to perserve user's input in the form.

Right now using your code below, I see the animation and the ContainerView fades out as it does not exist and I'm left with the UIViewController background and Buttons.

The code that I have is as follows:

- (IBAction)buttonOne:(id)sender {

ContainerViewSceneOne *controller = [[ContainerViewSceneOne alloc] init];

UIViewController *currentChild = [self.childViewControllers lastObject];
controller.view.frame = self.containerWindow.frame;

[UIView transitionFromView:self.containerWindow
toView:controller.view
duration:0.25f
options:UIViewAnimationOptionTransitionCrossDissolve
completion:^(BOOL finished) {
[currentChild removeFromParentViewController];
[self addChildViewController:controller];
}];
}

Here is link to the picture of the Storyboard:
http://stackoverflow.com/questions/14493893/scenes-to-open-inside-the-contianerview-through-uibuttons
This picture only shows 2 scenes for the ContainerView, but I actually have 5 as including all 5 scenes in the image would make all the images really small.

Thank you,
-Ahmed

January 26, 2013 | Unregistered CommenterAhmed

Thank you for these instructions. I found them very useful. I am creating an app with multiple views, actually 3 views, but if I unwind twice from 3 to 2 to 1 the last segue crashes the app with message -ABGViewController setResultE1:]: unrecognized selector sent to instance 0x751d4b0'- ResultE1 is a label on the 3rd view. This does not happen if I throw the destination of the second segue. What is happening. Can you help me? Thank you again.

January 27, 2013 | Unregistered Commentermark

Mark,

Remember, whenever you trigger a segue, the prepareForSegue:sender: method gets called. This happens both when doing a normal segue, and also when unwinding. We can use this to send data both upstream and back downstream.

So, my best guess is that the prepareForSegue:sender: method in view controller 2 is calling setResultE1: on the destination view controller. This then gets called correctly (when going from 2 to 3), and incorrectly (when going from 2 back to 1). In the second case, it leads to the crash.

To solve this, give all your segue's unique IDs, and check the ID before calling setResultE1:. Alternatively, don't worry about the segue IDs, and just check to make sure the destination controller responds to setResultE1: before calling it.

I hope that helps,

-Rich-

January 28, 2013 | Registered CommenterRichard Warren

Ahmed,

Ok, I think I've got it.

When you use the container view, it does two things. 1) it adds the referenced view controller to your container view controller as a child. 2) it adds the referenced controller's view to your container controller's view hierarchy, sizing it to fill the container view. The container view itself is just a place holder. It is replaced by the incoming view. That means if I create an outlet for my container view, I will get an outlet to the sub view controller's view property.

Now, my code does the following:

1) Removes the old sub view controller
2) Adds a new sub view controller
3) Animates the transition from the old controller's view to the new controller's view. This adds the new view to our view hierarchy and gets rid of the old view.
4) Unless you have other references to the old view controller, it will be deallocated. Having a reference to the old view may keep the view in memory--but without the controller, it really shouldn't be used for anything.

Does that make sense so far?

Here's the problem. In your storyboard, your sub view controller is itself a navigation view controller. You don't want to get rid of this view controller--you just want to push and pop the views onto it. If you try to use my code, the navigation controller actually gets removed (and probably deallocated), and then the rest of your code undoubtedly breaks.

So, instead of using my code, your button presses should do the following:

1) determine whether you need to push controllers onto the navigation controller, or pop them off of the navigation controller. You'll
probably have to look at the navigation controller's topViewController property or by calling count on its viewControllers property.

2) If your pushing views, you probably want to add all the intermediate views. For example, if I'm currently displaying view # 1, and someone presses button # 2, I'll need to do the following:


[navController pushViewController:viewController2 animated:NO];
[navController pushViewController:viewController3 animated:YES];


That way the previous button will still take me back to view 2.

3) if you're popping, you can simply pop back to the correct view controller. So, to go back from View Controller 5 to View Controller 3, you'd just call:


[navController popToViewController:viewController3 animated:YES];

I hope this helps,

-Rich-

January 28, 2013 | Registered CommenterRichard Warren

Hi Rich,

Sorry about the late reply. I was tied up and also scratching my head for a while. It wasn't your code or your explanation, I'm newbie (I'm sure you can tell), and it took sometime for the light bulb to come on! I reread your post several times and did some googling before figuring out how to make it work.

Yes, your code does make sense!

The trouble I had was grabbing the ContainerView Navigation Controller. For that I had to use:
UINavigationController *navController = [self.childViewControllers objectAtIndex:0];

Then I made an array of all the ViewControllers in that ContainerView:
NSMutableArray *VCs = [navController.viewControllers mutableCopy];

Last, I grabbed visibleViewController:
UIViewController *visibleViewController = [navController visibleViewController];

Then I went in to each button that was pressed and wrote the code. Below is the code for B2Pressed: Not sure if there is better way to refactor that, but it works!

- (IBAction)B2Pressed:(id)sender {
UINavigationController *navController = [self.childViewControllers objectAtIndex:0];
NSMutableArray *VCs = [navController.viewControllers mutableCopy];
UIViewController *visibleViewController = [navController visibleViewController];

int idx = [VCs indexOfObject:visibleViewController];

if (idx == 0)
{
B2 *b2 = [self.storyboard instantiateViewControllerWithIdentifier:@"B2"];
[navController pushViewController:b2 animated:YES];
} else if (idx == 1)
{
return;
} else
{
int i;
for (i = 2; i < idx; i++) {
[navController popViewControllerAnimated:NO];
}
[navController popViewControllerAnimated:YES];
}

Thank you so very much for your swift replies and detailed explanation!

February 9, 2013 | Unregistered CommenterAhmed

In the great example above you say "We can use either of these steps to pass data back to the return point." but you don't show how you do this.

Can you give a code example?

May 5, 2013 | Unregistered CommenterMel Malinowski

Hi,

It is a great post, I'm currently working on custom container controller and your post will help me a lot, however I have one question:

In following code:

[UIView
transitionFromView:self.containerView
toView:controller.view
duration:0.25f
options:UIViewAnimationOptionTransitionCrossDissolve
completion:^(BOOL finished) {
[currentChild removeFromParentViewController];
[self addChildViewController:controller];
}];

Shouldn't we call [currentChild willMoveToParentViewController:nil]; and [controller didMoveToParentViewController:self]; ? The documentation says that we should always call this methods when adding or removing child view controllers.

Thank you !
Stefan

August 22, 2013 | Unregistered CommenterStefan Ogonek

Stefan,

Sorry it took so long to reply. I just ran a quick test. It looks like calling removeFromParentViewController: automatically triggers didMoveToParentViewController: and addChildViewController: automatically calls willMoveToParentViewController:.

This is a bit confusing. Why do they only call half the required methods? It may be a bug. However, you are correct. We should probably add the missing calls ourselves.

September 19, 2013 | Registered CommenterRichard Warren

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>