CustomUnwindSegue/QuizContainerViewController.m

/*
     File: QuizContainerViewController.m
 Abstract: A custom container view controller that is functionally similar to a 
 UINavigationController.
 
  Version: 1.0
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2013 Apple Inc. All Rights Reserved.
 
 */
 
#import "QuizContainerViewController.h"
#import "QuizContainerFadeViewControllerSegue.h"
 
#import <QuartzCore/QuartzCore.h>
 
@interface QuizContainerViewController () <UINavigationBarDelegate> {
    UINavigationBar *_navigationBar;
    NSArray *_viewControllers;
}
@end
 
 
@implementation QuizContainerViewController
 
//| ----------------------------------------------------------------------------
//  We provide our own view.
//
- (void)loadView
{
    self.view = [[UIView alloc] init];
    
    _navigationBar = [[UINavigationBar alloc] init];
    _navigationBar.delegate = self;
    [self.view addSubview:_navigationBar];
}
 
 
//| ----------------------------------------------------------------------------
- (void)viewDidLoad
{
    // The transition animation involves fading out the outgoing view
    // controller's view while fading in the incoming view controller's view.
    // This creates a short 'flash' animation where the color of the flash is 
    // our view's backgroundColor.
    self.view.backgroundColor = [UIColor whiteColor];
    
    // This segue creates our initial root view controller.  Users of this
    // class are expected to define a segue named 'RootViewController' with
    // segue type QuizContainerRootViewControllerSegue.  The destination
    // of this segue should be the scene of the desired initial root view
    // controller.
    [self performSegueWithIdentifier:@"RootViewController" sender:self];
}
 
 
//| ----------------------------------------------------------------------------
//! Returns the appropriate frame for displaying a child view controller.
//
- (CGRect)frameForTopViewController
{
    return CGRectMake(0,
                      _navigationBar.frame.size.height + _navigationBar.frame.origin.y,
                      self.view.bounds.size.width,
                      self.view.bounds.size.height - _navigationBar.frame.size.height - _navigationBar.frame.origin.y);
}
 
 
//| ----------------------------------------------------------------------------
- (void)viewDidLayoutSubviews
{
    [_navigationBar sizeToFit];
    
    // Offset the navigation bar to account for the status bar.
    CGFloat topLayoutGuide = 0.0f;
    if ([self respondsToSelector:@selector(topLayoutGuide)])
        topLayoutGuide = [self.topLayoutGuide length];
    _navigationBar.frame = CGRectMake(_navigationBar.frame.origin.x, topLayoutGuide,
                                      _navigationBar.frame.size.width, _navigationBar.frame.size.height);
    
    self.topViewController.view.frame = [self frameForTopViewController];
}
 
#pragma mark -
#pragma mark Unwind Segue
 
//| ----------------------------------------------------------------------------
//! Returns the view controller managed by the receiver that wants to handle
//! the specified unwind action.
//
//  This method is called when either unwind segue is triggered in
//  ResultsViewController.  It is the responsibility of the parent of the
//  view controller that triggered the unwind segue to locate a
//  view controller that responds to the unwind action for the triggered segue.
//  
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
    // Like UINavigationController, search the array of view controllers
    // managed by this container in reverse order.
    for (UIViewController *vc in [_viewControllers reverseObjectEnumerator])
        // Always use -canPerformUnwindSegueAction:fromViewController:withSender:
        // to determine if a view controller wants to handle an unwind action.
        if ([vc canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender])
            return vc;
    
    // Always invoke the super's implementation if no view controller managed
    // by this container wanted to handle the unwind action.
    return [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
}
 
 
//| ----------------------------------------------------------------------------
//! Returns a segue object for transitioning to toViewController.
//
//  This method is called if the destination of an unwind segue is a child
//  view controller of this container.  This method returns an instance
//  of QuizContainerFadeViewControllerSegue that transitions to the destination
//  view controller of the unwind segue (toViewController).
//
- (UIStoryboardSegue*)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier
{
    // QuizContainerFadeViewControllerSegue is a UIStoryboardSegue subclass
    // for transitioning between view controllers managed by this container.
    QuizContainerFadeViewControllerSegue *unwindStoryboardSegue = [[QuizContainerFadeViewControllerSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
    
    // Set the unwind property to YES so an unwind animation is performed.
    // Note that this property is custom to QuizContainerFadeViewControllerSegue.
    unwindStoryboardSegue.unwind = YES;
    
    return unwindStoryboardSegue;
}
 
#pragma mark -
#pragma mark Actions
 
//| ----------------------------------------------------------------------------
//! Manual implementation of the topViewController property.
//! Returns the view controller at the top of the navigation stack.
//
- (UIViewController*)topViewController
{
    if (self.viewControllers.count == 0)
        return nil;
    return [self.viewControllers lastObject];
}
 
 
//| ----------------------------------------------------------------------------
//! Manual implementation of the getter for the viewControllers property.
//! Returns an array containing the view controllers currently on the
//! navigation stack.
//
- (NSArray*)viewControllers
{
    // This method is called by MainMenuViewController which then accesses
    // the first view controller in the array.  This occurs before our
    // view has been loaded (-viewDidLoad has not been called) which means
    // our initial root view controller has not been created yet (that happens
    // in -viewDidLoad).  But we must not return an empty array, so we force an
    // early load of our view here.
    [self view];
    
    return _viewControllers;
}
 
 
//| ----------------------------------------------------------------------------
//! Equivalent to calling -setViewControllers:animated: and passing NO for the
//! animated argument.
//
- (void)setViewControllers:(NSArray *)viewControllers
{
    [self setViewControllers:viewControllers animated:NO];
}
 
 
//| ----------------------------------------------------------------------------
//! Replaces the view controllers currently managed by the receiver with the
//! specified items.
//
//! @param  viewControllers
//!         The view controllers to place in the navigation stack.  The
//!         last item added to the array becomes the top item of the
//!         navigation stack.
//! @param  animated
//!         If YES, animate the pushing or popping of the top view controller.
//!         If NO, replace the view controllers without any animations.
//
//  This is where all of the transition magic happens. 
//
- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated
{
    // Compare the incoming viewControllers array to the existing navigation
    // stack, seperating the differences into two groups.
    NSArray *viewControllersToRemove = [_viewControllers filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", viewControllers]];
    NSArray *viewControllersToAdd = [viewControllers filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", _viewControllers]];
    
    for (UIViewController *vc in viewControllersToRemove)
        [vc willMoveToParentViewController:nil];
    
    for (UIViewController *vc in viewControllersToAdd)
        [self addChildViewController:vc];
    
    void (^finishRemovingViewControllers)() = ^() {
        for (UIViewController *vc in viewControllersToRemove)
            [vc removeFromParentViewController];
    };
    
    void (^finishAddingViewControllers)() = ^() {
        for (UIViewController *vc in viewControllersToAdd)
            [vc didMoveToParentViewController:self];
    };
    
    // The view controller presently at the top of the navigation stack.
    UIViewController *oldTopViewController = (_viewControllers.count) ? [_viewControllers lastObject] : nil;
    // The view controller that will be at the stop of the navgation stack.
    UIViewController *newTopViewController = (viewControllers.count) ? [viewControllers lastObject] : nil;
    
    // If the last object in the incoming viewControllers is the
    // already at the top of the current navigation stack then don't 
    // perform any animation as it would be redundant.
    if (oldTopViewController != newTopViewController)
    {
        if (oldTopViewController)
        {
            // Fade animations look wrong unless the root layer of the
            // animation rasterizes itself but be sure to remember the
            // old setting.
            BOOL oldTopViewControllerViewShouldRasterize = oldTopViewController.view.layer.shouldRasterize;
            oldTopViewController.view.layer.shouldRasterize = YES;
            
            // Fade out the old top view controller of the navigation stack.
            [UIView animateWithDuration:((animated) ? 0.25 : 0) delay:0 options:0 animations:^{
                oldTopViewController.view.alpha = 0.0f;
            } completion:^(BOOL finished) {
                // Restore the old shouldRasterize setting.
                oldTopViewController.view.layer.shouldRasterize = oldTopViewControllerViewShouldRasterize;
                [oldTopViewController.view removeFromSuperview];
                finishRemovingViewControllers();
            }];
        }
        else
            finishRemovingViewControllers();
        
        if (newTopViewController)
        {
            // Fade animations look wrong unless the root layer of the
            // animation rasterizes itself but be sure to remember the
            // old setting.
            BOOL newTopViewControllerViewShouldRasterize = newTopViewController.view.layer.shouldRasterize;
            newTopViewController.view.layer.shouldRasterize = YES;
            
            newTopViewController.view.frame = [self frameForTopViewController];
            [self.view addSubview:newTopViewController.view];
            
            newTopViewController.view.alpha = 0.0f;
            
            // Fade in the new top view controller of the navigation stack.
            [UIView animateWithDuration:((animated) ? 0.25 : 0) delay:((animated) ? 0.3 : 0) options:0 animations:^{
                newTopViewController.view.alpha = 1.0f;
            } completion:^(BOOL finished) {
                // Restore the old shouldRasterize setting.
                newTopViewController.view.layer.shouldRasterize = newTopViewControllerViewShouldRasterize;
                finishAddingViewControllers();
            }];
            
        }
        else
            finishAddingViewControllers();
    }
    else
    // No animation required.
    {
        finishRemovingViewControllers();
        finishAddingViewControllers();
    }
    
    _viewControllers = viewControllers;
    
    // Update the stack of navigation items for the _navigationBar to
    // reflect the new navigation stack.
    NSMutableArray *newNavigationItemsArray = [NSMutableArray arrayWithCapacity:viewControllers.count];
    for (UIViewController *vc in viewControllers)
        [newNavigationItemsArray addObject:vc.navigationItem];
    [_navigationBar setItems:newNavigationItemsArray animated:animated];
}
 
 
//| ----------------------------------------------------------------------------
//! Pushes a view controller onto the receiver’s stack and updates the display.
//
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    // Replace the navigation stack with a new array that has viewController
    // apeneded to it.
    [self setViewControllers:[self.viewControllers arrayByAddingObject:viewController] animated:animated];
}
 
 
//| ----------------------------------------------------------------------------
//! Pops view controllers until the specified view controller is at the top of the navigation stack.
//
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    // Check that viewController is in the navigation stack.
    NSUInteger indexOfViewController = [_viewControllers indexOfObject:viewController];
    if (indexOfViewController == NSNotFound)
        return nil;
    
    NSArray *viewControllersThatWerePopped = [_viewControllers subarrayWithRange:NSMakeRange(indexOfViewController+1, _viewControllers.count - (indexOfViewController+1))];
    NSArray *newViewControllersArray = [_viewControllers subarrayWithRange:NSMakeRange(0, indexOfViewController+1)];
    
    // Replace the navigation stack with a new array containg only the view
    // controllers up to the specified viewController.
    [self setViewControllers:newViewControllersArray animated:YES];
    
    return viewControllersThatWerePopped;
}
 
@end