26

Let ContainerView be the parent container view with two child content views: NavigationView and ContentView.

Example of View Layout

I would like to be able to swap out the controller of ContentView with another view. For example, swapping a home page controller with a news page controller. Currently, the only way I can think to do this is by using a delegate to tell the ContainerView that I want to switch views. This seems like a sloppy way to do this because the ContainerViewController would end up having a bunch of special delegates for all of the subviews.

This also needs to communicate with the NavigationView which has information about which view is currently in the ContentView. For example: if the user is on the news page, the navigation bar within the navigation view will show that the news button is currently selected.

Question A: Is there a way to swap the controller in ContentView without a delegate method calling the ContainerView itself? I would like to do this programmatically (no storyboard).

Question B: How can I swap controllers in ContentView from the NavigationView without a delegate call? I would like to do this programmatically (no storyboard).

hbk
  • 10,908
  • 11
  • 91
  • 124
Alex
  • 1,233
  • 2
  • 17
  • 27
  • 2
    Did you take a look at UIPageViewController? "A page view controller lets the user navigate between pages of content, where each page is managed by its own view controller object" – shadowhorst Oct 03 '13 at 15:26

3 Answers3

41

When you have child views that have their own view controllers, you should be following the custom container controller pattern. See Creating Custom Container View Controllers for more information.

Assuming you've followed the custom container pattern, when you want to change the child view controller (and its associated view) for the "content view", you would do that programmatically with something like:

UIViewController *newController = ... // instantiate new controller however you want
UIViewController *oldController = ... // grab the existing controller for the current "content view"; perhaps you maintain this in your own ivar; perhaps you just look this up in self.childViewControllers

newController.view.frame = oldController.view.frame;

[oldController willMoveToParentViewController:nil];
[self addChildViewController:newController];         // incidentally, this does the `willMoveToParentViewController` for the new controller for you

[self transitionFromViewController:oldController
                  toViewController:newController
                          duration:0.5
                           options:UIViewAnimationOptionTransitionCrossDissolve
                        animations:^{
                            // no further animations required
                        }
                        completion:^(BOOL finished) {
                            [oldController removeFromParentViewController]; // incidentally, this does the `didMoveToParentViewController` for the old controller for you
                            [newController didMoveToParentViewController:self];
                        }];

When you do it this way, there's no need for any delegate-protocol interface with the content view's controller (other than what iOS already provides with the Managing Child View Controllers in a Custom Container methods).


By the way, this assumes that the initial child controller associated with that content view was added like so:

UIViewController *childController = ... // instantiate the content view's controller any way you want
[self addChildViewController:childController];
childController.view.frame = ... // set the frame any way you want
[self.view addSubview:childController.view];
[childController didMoveToParentViewController:self];

If you want a child controller to tell the parent to change the controller associated with the content view, you would:

  1. Define a protocol for this:

    @protocol ContainerParent <NSObject>
    
    - (void)changeContentTo:(UIViewController *)controller;
    
    @end
    
  2. Define the parent controller to conform to this protocol, e.g.:

    #import <UIKit/UIKit.h>
    #import "ContainerParent.h"
    
    @interface ViewController : UIViewController <ContainerParent>
    
    @end
    
  3. Implement the changeContentTo method in the parent controller (much as outlined above):

    - (void)changeContentTo:(UIViewController *)controller
    {
        UIViewController *newController = controller;
        UIViewController *oldController = ... // grab reference of current child from `self.childViewControllers or from some property where you stored it
    
        newController.view.frame = oldController.view.frame;
    
        [oldController willMoveToParentViewController:nil];
        [self addChildViewController:newController];
    
        [self transitionFromViewController:oldController
                          toViewController:newController
                                  duration:1.0
                                   options:UIViewAnimationOptionTransitionCrossDissolve
                                animations:^{
                                    // no further animations required
                                }
                                completion:^(BOOL finished) {
                                    [oldController removeFromParentViewController];
                                    [newController didMoveToParentViewController:self];
                                }];
    }
    
  4. And the child controllers can now use this protocol in reference to the self.parentViewController property that iOS provides for you:

    - (IBAction)didTouchUpInsideButton:(id)sender
    {
        id <ContainerParent> parentViewController = (id)self.parentViewController;
        NSAssert([parentViewController respondsToSelector:@selector(changeContentTo:)], @"Parent must conform to ContainerParent protocol");
    
        UIViewController *newChild = ... // instantiate the new child controller any way you want
        [parentViewController changeContentTo:newChild];
    }
    
Dulgan
  • 6,674
  • 3
  • 41
  • 46
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • This is what I've seen in most container view implementations on GitHub and have inferred this from the documentation. Is there a way to do this without having this code duplicated in every controller? – Alex Oct 03 '13 at 16:00
  • 1
    @orbv This code is only in the parent controller, so there is no duplication involved. Perhaps I'm not understanding your question. – Rob Oct 03 '13 at 16:02
  • 1
    Xcode 5 just lets you use an Embed segue when adding a container view. I'd also avoid setting frames directly if auto-layout is being used. – Abizern Oct 03 '13 at 16:10
  • @Rob If that code is only in the parent controller, how can I call it from the child? – Alex Oct 03 '13 at 16:11
  • 1
    @Abizern Agreed. If targeting iOS 6 and above, and using storyboards, that's what I'd do. But OP said he wanted to do it programmatically with no storyboard. – Rob Oct 03 '13 at 16:19
  • 1
    @orbv Then you would define a protocol for that, and then have the child call the appropriate method in the parent view controller. That way, nice clean interface defined by protocol, but no duplication of the complicated custom container calls in all of your child controllers. See revised answer. – Rob Oct 03 '13 at 16:42
  • 2
    @Rob I see what you mean, after reading the question again. Just throwing it out there, though. ;) – Abizern Oct 03 '13 at 20:58
2

For such kind of transition you can also use UIView.animateWith... animations.

For example, assume that rootContainerView is container and contentViewController currently active controller in container, then

func setContentViewController(contentViewController:UIViewController, animated:Bool = true) {
    if animated == true {
        addChildViewController(contentViewController)
        contentViewController.view.alpha = 0
        contentViewController.view.frame = rootContainerView.bounds
        rootContainerView.addSubview(contentViewController.view)
        self.contentViewController?.willMoveToParentViewController(nil)

        UIView.animateWithDuration(0.3, animations: {
            contentViewController.view.alpha = 1
            }, completion: { (_) in
                contentViewController.didMoveToParentViewController(self)

                self.contentViewController?.view.removeFromSuperview()
                self.contentViewController?.didMoveToParentViewController(nil)
                self.contentViewController?.removeFromParentViewController()
                self.contentViewController = contentViewController
        })
    } else {
        cleanUpChildControllerIfPossible()

        contentViewController.view.frame = rootContainerView.bounds
        addChildViewController(contentViewController)
        rootContainerView.addSubview(contentViewController.view)
        contentViewController.didMoveToParentViewController(self)
        self.contentViewController = contentViewController
    }
}

// MARK: - Private

private func cleanUpChildControllerIfPossible() {
    if let childController = contentViewController {
        childController.willMoveToParentViewController(nil)
        childController.view.removeFromSuperview()
        childController.removeFromParentViewController()
    }
}

this will provide u simple fade animations, u can also can try any UIViewAnimationOptions, transitions etc.

hbk
  • 10,908
  • 11
  • 91
  • 124
-1

You seem to be getting confused. A contentView (assuming UIView) does not 'contain' a controller that you would swap out. A UIViewController handles its UIView's. It seems to me that you require a parent-child view controller setup.

A single parent view controller, that would handle child view controllers, each of which you can then handle when each is shown on the screen and adjust your UIViews and content accordingly. Please see the Apple documentation below.

Container Programming - Apple Documentation

Tim
  • 8,932
  • 4
  • 43
  • 64