Search code examples
iosiphoneuisplitviewcontrolleruipangesturerecognizer

UISplitViewController pan to primary view from anywhere


Sorry for the long-winded explination, but this question - or something similar - has been asked a few times and I havent found a satisfactory answer. I am writing an iPad app in iOS 8 that implements UISplitViewController. Recently I have been attempting to get it to work on the iPhone. It transferred over pretty well, everything collapses automatically and a back button is included in the left side of my nav. bar.

My problem is that I want to keep the back button functionality to pop one view off the stack, but also be able to pan back to the primary view even if there are several detail views on top of it. Ideally, I want to be able to overwrite or redirect the interactivePopGestureRecognizer so that the gesture smoothly pans to the primary view (in some cases it can have anywhere from 1 to 4 detail views stacked on top of it). But, I cannot figure out how to do this.

My current solution (code below) is to disable the interactivePopGestureRecognizer in the detail viewcontroller and implement my own ScreenEdgePanGestureRecognizer that, when triggered, executes popToRootViewController. I've subclassed the ScreenEdgePanGestureRecognizer so it treats the screen edge pan as a discrete "swipe" (i.e. once a large enough screen edge swipe is detected - pop everything off the stack so the primary view is visible).

Code in detail view controller to stop interactivePopGestureRecognizer:

-(void)viewWillAppear : (BOOL) animated {

    [super viewWillAppear : animated];
    // stops navigation controller from responding to the default back swipe gesture
    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.navigationController.interactivePopGestureRecognizer.enabled =NO;
        self.navigationController.interactivePopGestureRecognizer.delegate = self;
    }
}

// Disable the default back swipe gesture tied to automatically included back button
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {

    if ([gestureRecognizer isEqual:self.navigationController.interactivePopGestureRecognizer]) {
        return NO;
    } else {
        return YES;        
    }

}

I didn't think it was necessary to include my subclass for the screenEdgePanGestureRecognizer because it has nothing to do with the solution I am asking about here is some pseudocode that shows what my @selector does in the detail viewcontroller:

- (IBAction)leftEdgeSwipe:(ScreenEdgeSwipeGestureRecognizer*)sender {
    if (sender.swipeIsValid) {
        [(UINavigationController *)self.splitViewController.viewControllers[0]
         popToRootViewControllerAnimated:YES];
    }
}

I tried to use the continuous pan, but cannot find a way to present the primary view in the background as I am pulling the current view aside to give that clean, smooth panning effect. I am able to make it so I can move the current view around, but there is just a grey background behind it where I would want my primary view to be.

Summation: If there is indeed no way to change the interactivePopGestureRecognizer to always jump to my primary view (ideal solution), then any info on how I can make my own smooth pan back to my primary view would be much appreciated.


Solution

  • So I have been messing around with making a smooth panning gesture subclass. Currently it functions similarly to Apple's back gesture except it jumps all the way back to the root view controller instead of popping one view off the stack. The only problem is that it does not yet show the primary view in the background while panning. I will update the answer once I get that worked out.

    Here is the subclass:

    #import <UIKit/UIKit.h>
    #import <UIKit/UIGestureRecognizerSubclass.h>
    #import "ScreenEdgeSwipeGestureRecognizer.h"
    
    
    
    @interface ScreenEdgeSwipeGestureRecognizer ()
    
    @property (nonatomic) UINavigationController* navController;
    
    @end
    
    
    @implementation ScreenEdgeSwipeGestureRecognizer{
        CGPoint _screenCenter;
        CGPoint _cumulativePanDistance;
    }
    
    
    
    - (id)initWithNavigationController:(UINavigationController*)navController {
        self = [super initWithTarget:self action:@selector(leftEdgePan:)];
        _screenCenter = CGPointZero;
        _cumulativePanDistance = CGPointZero;
        self.edges = UIRectEdgeLeft;
        self.navController = navController;
        return self;
    }
    
    
    
    - (IBAction)leftEdgePan:(ScreenEdgeSwipeGestureRecognizer*)sender {
        assert(sender == self);
    
        switch (self.state) {
            case UIGestureRecognizerStateBegan:
                [self initializePositions];
                break;
    
            case UIGestureRecognizerStateChanged:
                [self updatePositions];
                break;
    
            case UIGestureRecognizerStateEnded:
                [self animateViewBasedOnCurrentLocation];
                break;
    
            case UIGestureRecognizerStateCancelled:
                [self animateViewToCenter];
                break;
    
            default:
                break;
        }
    
        // Reset velocity of the pan so current velocity does not compound with velocity of next cycle
        [sender setTranslation:CGPointMake(0, 0) inView:sender.view];
    }
    
    
    
    - (void)initializePositions {
        _screenCenter = self.view.center;
        _cumulativePanDistance = CGPointZero;
    }
    
    
    
    - (void)updatePositions {
        // Track position of user touch event
        CGPoint deltaSinceLastCycle = [self translationInView:self.view];
    
        // View center = view center at last cycle + distance moved by user touch since last cycle
        self.view.center=CGPointMake((self.view.center.x +   deltaSinceLastCycle.x), self.view.center.y+ 0);
    
        // Update the total positive distance traveled by the user touch event.
        _cumulativePanDistance.x = _cumulativePanDistance.x + deltaSinceLastCycle.x;
    }
    
    
    
    - (void)animateViewBasedOnCurrentLocation {
        if (_cumulativePanDistance.x >= (_screenCenter.x - 50)){
            [self reset];
            [_navController popToRootViewControllerAnimated:YES];
        }else{
            [self animateViewToCenter];
            [self reset];
        }
    }
    
    
    
    - (void)animateViewToCenter {
        [UIView animateWithDuration:0.25 animations:^{self.view.center = self->_screenCenter;}];
    }
    
    
    
    - (void)reset {
        [super reset];
        _cumulativePanDistance = CGPointZero;
        self.state = UIGestureRecognizerStatePossible;
    }
    
    @end
    

    Here is how I instantiate the recognizer in my view controller:

    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
    
        // Initialize the screen edge pan gesture recognizer.
        _masterNavigationController = self.splitViewController.viewControllers[0];
    
        ScreenEdgePanGestureRecognizer* edgePanRecognizer =  [[ScreenEdgeSwipeGestureRecognizer alloc] initWithNavigationController:_masterNavigationController];
    
        // Add recognizer to view this controller is bound to.
        [self.view addGestureRecognizer:_edgePanRecognizer];
    }