developing a simple ios sliding navigation app

Mon, Aug 26, 2013

I’m working on an iOS app that lets me identify mountains based on my location and bearing but one of the first things I wanted to tackle was the navigation. How to get from screen to screen. Initially I tried a tabbed app layout but that seems so ‘yesterday’ these days and instead I plumped for a sliding navigation model, ala Facebook app. It’s not as complicated as it sounds once you’ve got your head round iOS development so this post takes you through constructing an app with three coloured views. Red, green and blue with a central controller handling the swiping to navigate among them.

I’ve adapted the code from the brilliant Tammy Coron tutorial on the incomparable Ray Wenderlich site. The original tutorial lets you swipe right and left but I only wanted to swipe one way to reveal a navigation panel and so didn’t want the main view sliding to the right at all. I also wanted the view slid back into place when a choice had been made in the navigation options. Hence the modifications.

So where do we start? At the beginning! The first thing we need to do is set up our development environment. I much prefer keeping different types of classes and assets in their own folders so the following video is how to create custom folders in XCode 4.6.1 and create new files in them. The process is a bit long winded as you have to create the folder first then create files in it but the video explains all. It also shows you how to wire up views to their controllers without using Storyboards. I wanted to stay fairly low level for this so created custom views for each screen with their own controllers wired up later. When wiring up, the key is to make sure each UIView has a referencing outlet back to its parent’s File’s Owner but again, the video show you how.

If you don’t reference the view in the File’s Owner you’ll get this error:

this class is not key value coding-compliant for the key …’

Anyway, here’s the video:

Now that’s out of the way, this is what we’re going to build. A simple app with a red, green and blue screen with corresponding buttons to select them in a hidden navigation panel. To reveal the navigation panel you either tap on the navigation button in a screen or swipe right to reveal it and swipe left to hide it. Selecting a coloured button will hide the navigation panel and slide in the selected panel.

app

We’ll need a way for the panels to talk to the main controller, so we need a delegate protocol:


@protocol PanelDelegate <NSObject>

@required
// A child panel calls these methods in its delegate when the nav button is clicked
// Which one is called depends on the tag of the navButton, which toggles each time
// it's tapped.
- (void)movePanelRight;
- (void)movePanelToOriginalPosition;

// NavigationViewController calls this method in its delegate when one of the
// coloured buttons is tapped.
- (void)didSelectViewWithName:(NSString *)viewName;

@end

and some basic plumbing in a panel:


#import "PanelDelegate.h"

@interface RedViewController : UIViewController

@property (nonatomic, assign) id<PanelDelegate> delegate;
@property (weak, nonatomic) IBOutlet UIButton *navButton;

- (IBAction)navButtonClicked:(id)sender;

@end

When navButtonClicked: is invoked the panel tells its delegate to do something, based on the tag value of the navButton in the view:


// navButton tag = 1 when created in Interface Builder
- (IBAction)navButtonClicked:(id)sender {
    UIButton *button = sender;
    switch (button.tag) {
        case 0: {
            self.navButton.tag = 1;
            [_delegate movePanelToOriginalPosition];
            break;
        }
            
        case 1: {
            self.navButton.tag = 0;
            [_delegate movePanelRight];
            break;
        }
            
        default:
            break;
    }
}

So when you tap on the chevrons the currently selected panel will slide to the right to reveal the navigation panel. Tap it again and it will slide back to the left, hiding the navigation. The code to slide the panel to the right is quite simple:


// Called by a view when its navButton is clicked and the panel is occupying the entire screen
- (void)movePanelRight {
    UIView *childView = [self getNavigationView];
    [self.view sendSubviewToBack:childView];
    
    [UIView animateWithDuration:SLIDE_TIMING delay:0 options:UIViewAnimationOptionBeginFromCurrentState
                     animations:^{
                         activeViewController.view.frame = CGRectMake(self.view.frame.size.width - PANEL_WIDTH, 0, self.view.frame.size.width, self.view.frame.size.height);
                     }
                     completion:^(BOOL finished) {
                         if (finished) {
                         }
                     }];
}

It just starts from the current position and animates the panel rightwards until the predefined position is reached.

Back in the main controller we create and wire up the first panel to show:


- (void)setupView {
    // When the app is launched we'll start by showing the red view
    _redViewController = [[RedViewController alloc] initWithNibName:@"RedView" bundle:nil];
    _redViewController.delegate = self;
    [self.view addSubview:_redViewController.view];
    [self addChildViewController:_redViewController];
    [_redViewController didMoveToParentViewController:self];
    activeViewController = _redViewController;
}

and add the code to handle navigation events from the coloured buttons:


// Called by NavigationViewController when one of the coloured buttons is tapped
- (void)didSelectViewWithName:(NSString *)viewName {
    if (activeViewController != nil) {
        [activeViewController.view removeFromSuperview];
        activeViewController = nil;
    }
    [self showActiveViewWithName:viewName];
    [self movePanelToOriginalPosition];
}

The above code replaces the currently active panel with the newly selected one, so if you keep your eyes on it when it’s off to the side you’ll see it change. The last line of code then slides the newly selected panel into position. How does it do that? Same as sliding it right but in the opposite direction:


// Called by a view when its navButton is clicked and the panel has already been moved to the right
- (void)movePanelToOriginalPosition {
    [UIView animateWithDuration:SLIDE_TIMING delay:0 options:UIViewAnimationOptionBeginFromCurrentState
                     animations:^{
                         activeViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
                     }
                     completion:^(BOOL finished) {
                         if (finished) {
                             [self resetMainView];
                         }
                     }];
}

We then finish by resetting the main view to release the now hidden navigation panel:


- (void)resetMainView {
    if (_navigationViewController != nil) {
        [_navigationViewController.view removeFromSuperview];
        _navigationViewController = nil;
    }
    
    [self showActiveViewWithShadow:NO withOffset:0];
}

So that’s how you implement a basic sliding navigation app but the best is yet to come. What about dragging the panels aside to reveal the navigation? For that we need a UIPanGestureRecognizer.

So far we’ve been handling events in the child views, i.e. the coloured panels and the navigation view but now we’ll handle dragging in the main view. What’s going to happen is the main view will detect the gestures and animate the currently active panel accordingly. So first we need to hook into the gestures as a recogniser:


-(void)setupGestures {
    UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(movePanel:)];
    [panRecognizer setMinimumNumberOfTouches:1];
    [panRecognizer setMaximumNumberOfTouches:1];
    [panRecognizer setDelegate:self];
    
    [self.view addGestureRecognizer:panRecognizer];
}

We create a new UIPanGestureRecognizer, set the main controller as the delegate and tell it to invoke movePanel: when something happens. Now for the interesting bit, sliding the views around on a whim:


// This is where we can slide the active panel from left to right and back again,
// endlessly, for great fun!
-(void)movePanel:(id)sender {
    [[[(UITapGestureRecognizer*)sender view] layer] removeAllAnimations];
    
    CGPoint translatedPoint = [(UIPanGestureRecognizer*)sender translationInView:self.view];
    CGPoint velocity = [(UIPanGestureRecognizer*)sender velocityInView:[sender view]];
    
    // Stop the main panel from being dragged to the left if it's not already dragged to the right
    if ((velocity.x < 0) && (activeViewController.view.frame.origin.x == 0)) {
        return;
    }
    
    if ([(UIPanGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
        if(velocity.x > 0) {
            _showPanel = YES;
        }
        else {
            _showPanel = NO;
        }
        
        UIView *childView = [self getNavigationView];
        [self.view sendSubviewToBack:childView];
    }
    
    if ([(UIPanGestureRecognizer*)sender state] == UIGestureRecognizerStateEnded) {
        // If we stopped dragging the panel somewhere between the left and right
        // edges of the screen, these will animate it to its final position.
        if (!_showPanel) {
            [self movePanelToOriginalPosition];
            _panelMovedRight = NO;
        } else {
            [self movePanelRight];
            _panelMovedRight = YES;
        }
    }
    
    if ([(UIPanGestureRecognizer*)sender state] == UIGestureRecognizerStateChanged) {
        if(velocity.x > 0) {
            _showPanel = YES;
        }
        else {
            _showPanel = NO;
        }
        
        // Set the new x coord of the active panel...
        activeViewController.view.center = CGPointMake(activeViewController.view.center.x + translatedPoint.x, activeViewController.view.center.y);
        
        // ...and move it there
        [(UIPanGestureRecognizer*)sender setTranslation:CGPointMake(0, 0) inView:self.view];
    }
}

We handle gestures in a three part manner. UIGestureRecognizerStateBegan, UIGestureRecognizerStateChanged and UIGestureRecognizerStateEnded.

So the user taps and holds on the currently active panel and starts dragging it from side to side, hopefully with a huge grin on their face. When they finally get bored and release their vice like poke on the screen, we decide which way they were going (_showPanel) when they released and finish the animation for them. This means they don’t leave the panel in the middle of the screen. If they release it while swiping left, the panel will glide over to the far left and hide the navigation view and vice versa. The check at the start of the method is to prevent them sliding the panel to the left when it’s already as far left as it will go, otherwise a lot of blank screen will appear.

So that’s that, a simple sliding navigation app. There are prolly far more sophisticated ways of doing it but this shows how to get started and is also a great route into wiring up custom views and sliding them around.

You can download the entire project from my gihub repo.

comments powered by Disqus