May 032013
 

If you’ve been coding iOS for even a short period of time you’ve come across presentViewController. (If you are using a UINavigationViewController, you have pushViewController.) You are limited, however, in the animations you can use: cover vertical, flip horizontal, or cross dissolve.

I have a client who need left, right, up and down swipes. The app wasn’t a traditional drill-down then back back back out app. It was very free-form. So I needed to roll my own.

I call the class the EnkiUnorderedController. The Enki is from the name of my consulting company, Enki Labs, and the Unordered part refers to the random access nature of the UIViewControllers it displays. Lets look at the class interface first. (The source is all available in the open source EnkiUtils project, but the source there has comments in it!)

@interface EnkiUnorderedController : UIViewController
@property (strong, nonatomic)  UIViewController *current;
@property (strong, nonatomic)  UIViewController *cover;

- (void)initCurrentWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;
- (void) replaceViewController:(UIViewController *) target withDirection:(slideDirection) direction;
- (void) coverViewController:(UIViewController *) target withDirection:(slideDirection) direction;

- (void) uncoverViewControllerwithDirection:(slideDirection) direction;
@end

To use an EnkiUnorderedViewController you create one and initialize with the UIViewController you want it to display.

self.unorderedController =  [[EnkiUnorderedController alloc] init];
[_unorderedController initCurrentWithNibName:@"MAWJump1ViewController" bundle:nil];

In this example, I have a UIViewController subclass named MAWJump1ViewController and its layout is in the xib file with the same name. Lets look at initCurrentWithNibName’s implementation:

- (void)initCurrentWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    Class vcClass = NSClassFromString (nibNameOrNil);
    self.current = [[vcClass alloc] initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    
    if (self.current) {
        
        self.current.view.frame = self.view.frame;

        [self addChildViewController:self.current];
        [self.current didMoveToParentViewController:self];
        [self.view addSubview: self.current.view];
        
    }
}

Notice that it uses both reflection and container view controllers. Doing this pre-iOS5 would have been difficult!

The first thing I do is create an object based on the class name. (Yes, this means the class name must be the same as the xib file name.) I alloc one of these and call the expected initWithNibName on it. I store this in the property named current. (And right now, when writing this blog, I’m wondering if I should make current a class extension, so it is private.) I grab the frame of the containing class and then use the container view controller methods to install it as a child view controller.

So far this seems like an awful lot of work with no benefit. But, what happens when you want to replace the view controller with another one?

    MAWAppDelegate  * appDelegate = (MAWAppDelegate *)[[UIApplication sharedApplication] delegate];
    EnkiUnorderedController *unVC = appDelegate.unorderedController;
    
    [unVC replaceViewController:[self randomVC] withDirection:[self randomDirection]];

In my sample app I store the EnkiUnorderedViewController in the app delegate and simply make a call to replaceViewController to bring the new one in. (randomVC and randomDirection are methods in the sample app to give us a random view controller and a random direction, they aren’t really all that interesting, but see the sample code if you want to.) The direction is specified by an enum:

typedef enum {
    kSlideUndefined = 0,
    kSlideUp = 1,
    kSlideDown = 2,
    kSlideLeft = 3,
    kSlideRight = 4,
} slideDirection;

Lets look at replaceViewController’s implementation:

(slideDirection) direction;
{
    [self replaceViewController:target withDirection:direction withCover:NO];
}

- (void) coverViewController:(UIViewController *) target withDirection:(slideDirection) direction;
{
    [self replaceViewController:target withDirection:direction withCover:YES];
}

Ok, that isn’t too interesting. I’m just calling a private method that says if we are covering the view controller or not. (Covering is something the client’s app needed. When you replace a view controller the original one is gone, when you cover it, you can uncover it later and get it back, this is much like UINavigationController’s push and pop methods except that I haven’t written the stack yet.) Lets look at the real code:

- (void) replaceViewController:(UIViewController *) target withDirection:(slideDirection)direction withCover:(BOOL) coverIt;
{
    target.view.frame = self.view.frame;
   
    // add the target as a child view controller.
    [self addChildViewController:target];
    [target didMoveToParentViewController:self];
    [self.view addSubview: target.view];

    // knock the target off the screen to start with
    [self adjustTarget:target withDirection:direction];
    
    [UIView animateWithDuration:0.5
                          delay:0
                        options: UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         // slide the current one away
                         [self slideCurrent:_current withDirection:direction];
                         
                         // slide the target in
                         [self slideTarget:target withDirection:direction];                         
                     }
                     completion:^(BOOL finished){
                         if (coverIt) {
                             if (_cover != nil) {
                                 [_cover removeFromParentViewController];
                             }
                             _cover = target;
                         }else {
                             [_current removeFromParentViewController];
                             _current = target;
                         }
                     }];
}

We grab the target (the new one) and add it to the EnkiUnorderedViewController. So now the parent has 2 contained view controllers. We call the internal method adjustTarget just to get it off the screen. We use the animateWithDuration block method to slide the current view controller away and then slide the new one in. Finally, using the completion block will either remove the current view controller and set the target to current, or if we are covering we set the cover to target, leaving the current intact.

Yes, there is rectangle math in there also, it isn’t very interesting but download the full sample project if you want to see it.