iOS 自定义滑动返回和解决连续多次push,pop引起的crash问题

      UINavigationController的滑动返回作为iOS7的新特性被引入,但是只有在滑动视图的左边缘的时,才可以体验这样的交互。如果,想要滑动视图的任意位置,想要体验这样的交互,就需要自己动手处理了,后者使用第三方库。

     另外,最近的程序中遇到这样的一个问题:push到一个钱包界面,这个界面请求钱包的内容。这个界面比较敏感,涉及钱财,采取了一些安全措施。其中的一个就是客户端距离上次请求验证的接口超过有效时间,就会要求用户再次登陆。那么对于这里,就是在进入钱包界面的时候,请求数据,服务器端如果认为登陆状态失效了,就需要引导用户再次登陆。如果用户不愿意登陆,点击返回到钱包界面,因为用户没有再次登陆,为了安全起见,需要立刻跳转到钱包的前一个界面。如此来回快速的push和pop很容易引起crash,(如can't add self as subview等问题)。

    关于这个滑动返回还要涉及的一个问题就是navigationbar的颜色更改和隐藏与否。如果不能很好处理navigationbar的颜色更改,就有可能出现navigationbar的颜色是现在的颜色和下一个颜色的过渡色;当前界面不隐藏navigationbar,下一个界面隐藏,如果不能很好处理,就有可能出现下个界面因为要处理navigationbar而引起界面的跳动。

     观察了一些已有的app的,有些处理的很好,于是,就想尝试自己写一个。

                                                                               

     两个navigationbar的颜色不同,滑动返回也不会                     滑动返回中,上一个navigationbar上的文本与

    导致两个navigationbar串色。                                                     当前的重叠。

 

      1.分析滑动返回:

        滑动是一个手势。滑动返回是利用手势来控制当前视图的移动,另外,上一个界面也会因为当前界面右移而右移显示出来。这里需要用到截屏,也就是说上一个界面其实是截取的一个“图片”。

      2.分析连续的pop和push:

       连续的pop和push之所以会引起问题,是因为动画需要时间,在动画没有结束之前,来回返回push和pop就会引起异常,解决的方式是在一个UIViewController或者其子类的viewDidAppear方法里面进行push和pop操作,也就是说要视图完全呈现出来,动画完全结束后,再进行操作。

     解决方法:

   使用一个数组来保存所有的push和pop操作,在确定了viewDidAppear方法被调用后,再一次执行数组里面保存的操作。

   滑动返回则需要手势识别器UIPanGestureRecognizer,视图随着手势的移动而移动。

   

   首先是滑动返回:

<span style="font-family:KaiTi_GB2312;font-size:18px;">  UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
  [self.view addGestureRecognizer:panRecognizer];</span>
  

   处理手势:

<span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)handlePanGesture:(UIPanGestureRecognizer *)pan {
    
    if (!self.dragEnable || self.viewControllers.count < 2) {
        return;
    }
    
    
    CGPoint touchPoint = [pan locationInView:KEY_WINDOW];
    
    if (pan.state == UIGestureRecognizerStateBegan) {
        
        self.startTouchPoint = touchPoint;
        
        if (![self.view.superview.subviews containsObject:self.containerView]) {
            [self.view.superview insertSubview:self.containerView belowSubview:self.view];
        }
        
        
        if (![self.containerView.subviews containsObject:self.lastScreenImageView]) {
            [self.containerView addSubview:self.lastScreenImageView];
        }
    
        
        self.lastScreenImageView.image = [self.screenShots lastObject];
        CGRect frame = self.lastScreenImageView.frame;
        frame.origin.x = self.startX;
        frame.size = [[UIScreen mainScreen] bounds].size;
        self.lastScreenImageView.frame = frame;
        
        
        self.containerView.hidden = NO;
    }
    else if (pan.state == UIGestureRecognizerStateEnded) {
     
        if (touchPoint.x - self.startTouchPoint.x > CGRectGetWidth(self.view.frame) / 3.0) {
            
            [UIView animateWithDuration:self.animationDuration animations:^{
                
                [self __moveViewWithOffsetX:CGRectGetWidth(self.view.frame)];
                
            }completion:^(BOOL finished) {
                
                [self popViewControllerAnimated:NO];
                
                CGRect frame = self.view.frame;
                frame.origin = CGPointZero;
                self.view.frame = frame;
                
                self.containerView.hidden = YES;
            }];
        }
        else {
            
            [UIView animateWithDuration:self.animationDuration animations:^{
               
                [self __moveViewWithOffsetX:0];
                
            }completion:^(BOOL finished) {
                
                self.containerView.hidden = YES;
            }];
        }
        
        return;
    }
    else if (pan.state == UIGestureRecognizerStateCancelled) {
        
        [self __resetOriginalState];
        
        return;
    }
    
 
    [self __moveViewWithOffsetX:touchPoint.x - self.startTouchPoint.x];
}

</span>


移动视图界面:

<span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)__moveViewWithOffsetX:(CGFloat)x {
   
    x = MAX(MIN(CGRectGetWidth(self.view.frame), x), 0);
 
    
    CGRect frame = self.view.frame;
    frame.origin.x = x;
    self.view.frame = frame;
    
    
    CGFloat rate = fabsf(self.startX) / CGRectGetWidth(self.view.frame);
    CGFloat distance = rate * x;
    self.lastScreenImageView.frame = CGRectMake(self.startX + distance, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame));
}
</span>

当滑动界面没有达到滑动临界值时,所要把当前的界面设置为“原始”状态:

<span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)__resetOriginalState {
    
    [UIView animateWithDuration:self.animationDuration animations:^{
        
        [self __moveViewWithOffsetX:0];
        
    }completion:^(BOOL finished) {
        
        self.containerView.hidden = YES;
    }];
}</span>

在处理连续push和pop的时候,重写了pushViewController:animated:     popViewControllerAnimated:     popToViewControllerAnimated:   popToRootViewControllerAnimated:这四个方法


<pre name="code" class="objc"><span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    
    __weak typeof(self) weakSelf = self;
    
    [self __addTransitionBlock:^{
        
        [weakSelf.screenShots addObject:[weakSelf __captureScreen]];
        
        [super pushViewController:viewController animated:animated];
    }];
}

- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    
    __weak typeof(self)weakSelf = self;
    
    [self __addTransitionBlock:^{
        
        [weakSelf.screenShots removeLastObject];
        
        UIViewController *viewController = [super popViewControllerAnimated:animated];
        if (viewController == nil) {
            weakSelf.transitionInProcess = NO;
        }
    }];
    
    return nil;
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated {
    
    __weak typeof(self)weakSelf = self;
    
    [self __addTransitionBlock:^{
        
        if ([weakSelf.viewControllers containsObject:viewController]) {
            
            NSInteger index = [weakSelf.viewControllers indexOfObject:viewController];
            if (weakSelf.viewControllers.count > index) {
                [weakSelf.screenShots gf_removeObjectsFromIndex:index];
            }
            
            
            NSArray *viewControllers = [super popToViewController:viewController animated:animated];
            
            if (viewControllers.count == 0) {
                weakSelf.transitionInProcess = NO;
            }
        }
        else {
            weakSelf.transitionInProcess = NO;
        }
    }];
    
    return nil;
}

- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated {
 
    __weak typeof(self) weakSelf = self;
    
    [self __addTransitionBlock:^{
        
        if (weakSelf.screenShots.count) {
            [weakSelf.screenShots gf_removeObjectsFromIndex:0];
        }
        
        NSArray *viewControllers = [super popToRootViewControllerAnimated:animated];
        
        if (viewControllers.count == 0) {
            weakSelf.transitionInProcess = NO;
        }
    }];
    
    return nil;
}</span>

 

这里需要注意的是:正式因为把对NavigationController的堆栈操作都写到block里面,pop的一系列方法将没有返回值。

pendingBlocks是一个数组,保存了对navigationController堆栈的操作

<span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)__addTransitionBlock:(PendingBlock)block {

    if (self.transitionInProcess) {
        [self.pendingBlocks addObject:[block copy]];
    }
    else {
        _transitionInProcess = YES;
        block();
    }
}

- (void)__runNextTransition {
    
    if (self.pendingBlocks.count) {
        PendingBlock block = [self.pendingBlocks lastObject];
        [self.pendingBlocks removeLastObject];
        block();
    }
}</span>

另外使用transitionInProcess来标记当前是否有navigationcontroller堆栈的操作

<span style="font-family:KaiTi_GB2312;font-size:18px;">@property (nonatomic, assign, getter = isTransitionInProcess) BOOL transitionInProcess;
</span>

<span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)setTransitionInProcess:(BOOL)transitionInProcess {
    _transitionInProcess = transitionInProcess;
    
    if (!_transitionInProcess && self.pendingBlocks.count) {
        _transitionInProcess = YES;
        [self __runNextTransition];
    }
}</span>

需要注意的一点是:整个布尔值需要在自己项目的BaseViewController 的viewDidAppear方法中设置为No,   因为一个视图已经展示了,可以运行下个操作了。我的项目中既视图控制器是GFBaseViewController, 你可以使用你的,只要在下面代码加入;或者都继承GFBaseViewController,就不用写下面的代码了。另外,需要在BaseViewController里面将self.view的背景色设置成白色,我是设置成白色了,当然,任何颜色都可以。但是,不要默认。

<span style="font-family:KaiTi_GB2312;font-size:18px;">- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    ((GFNavigationController *)self.navigationController).transitionInProcess = NO;
    
}</span>

这样就可以随心所欲在viewWillAppear等地方push,pop了

 
<span style="font-family:KaiTi_GB2312;">- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    //push
    
    UIViewController *controller = [[UIViewController alloc] init];
    [self.navigationController pushViewController:controller animated:YES];
}</span>

运行效果图:

   


  GFNavigationController代码


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值