码无界IOS And AndroidiOS进阶知识@IT·互联网

隔离导航控制器带来的坑(iOS)

2016-07-27  本文已影响359人  Devil雅馨

题外话:最近一直很闲,项目上基本没有啥大的需求。对于程序员来说,如果没有需求其实是一件很难受的事情,之前好多次在项目中没事找事,该优化的优化,该整理的整理。可能好多程序员都遇到过与我类似的情况。但是程序员真的需要通过刷项目去提高自己吗?程序员的功力体现在处理项目的细节上,有可能为了改进一点点的体验就要付出很大的代价,就要接触更多的知识点。做的项目多,但是不精那么只能代表自己在比较浅显的领域比较熟练而已。以上观点是我在最近思考得来的,并不一定正确,甚至明天一早我就会推翻这种想法,请大家谨慎参考😄。

一、什么是隔离导航控制器?

这个名词纯粹是我自己瞎编的,我不知道有没有这种说法,至少我没有看到。我所说的隔离导航控制器是指:在导航栏样式不同的页面采用不同的导航控制器。现在同一个导航栏发生变化主要体现在:隐藏导航栏页面跳转到非隐藏导航栏的页面、A颜色导航栏的页面跳转到B颜色导航栏的页面、可跟随滑动做动画的页面跳转到固定导航栏的页面。隔离导航控制器就是:当页面跳转到导航栏不同的页面时不再使用同一个导航控制器,而是弹出一个新的导航控制器,这个导航控制器的导航栏固定不变,如果发生变化那么再弹出一个新的导航控制器。

二、为什么要隔离导航控制器?

很简单,因为我已经受够了页面的来回跳转导致状态栏储存、变化、恢复这样的过程。尤其在侧滑或者全屏滑动返回时,导航栏的变化会有不好的体验。所以在很久以前我就在酝酿,如果present出来一个导航控制器就好了(隔离导航控制器并不是一个很好地解决办法,但是可以试一试,毕竟我很闲)。因此我在项目中试了一下,结果遇到了一些问题,自己挖的坑,跪着也要填满了。

三、实现右进右出的present

ViewController 的present样式一共有4种:

typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
    UIModalTransitionStyleCoverVertical = 0,//默认
    UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,//翻转
    UIModalTransitionStyleCrossDissolve,//透明度渐变
    UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,//翻书
};
[vc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];    //present 前加上这句就行
[self presentViewController:vc animated:YES completion:nil];

没有我们需要的效果,因此我们需要利用iOS 7 以后的转场动画来实现。说到转场动画不怕大家笑话,我已经看了好多遍了,但是依然记不住那些名字。每次要用都要去大神的博客里再学习一边。我的代码也是使用大神的代码,因为觉得没有必要再写一遍。只不过我做了略微的修改。

// NormalDismissAnimation
// 4. Do animate now
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    toVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是为了上下两个控制器有联动的效果
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        fromVC.view.frame = finalFrame;
        toVC.view.transform = CGAffineTransformIdentity;
 
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
 
 
//BouncePresentAnimation
  // 4. Do animate now
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        toVC.view.frame = finalFrame;
        fromVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是为了有上下两个控制器联动的效果
    } completion:^(BOOL finished) {
        fromVC.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:YES];
    }];
 
 
//SwipeUpInteractiveTransition
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
       switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan:
            // 1. Mark the interacting flag. Used when supplying it in delegate.
            self.interacting = YES;
            [self.presentingVC dismissViewControllerAnimated:YES completion:nil];
            break;
        case UIGestureRecognizerStateChanged: {
            // 2. Calculate the percentage of guesture
            CGSize screenSize = [UIScreen mainScreen].bounds.size;
            CGFloat fraction = translation.x / screenSize.width;
            //Limit it between 0 and 1
            fraction = fminf(fmaxf(fraction, 0.0), 1.0);
            self.shouldComplete = (fraction > 0.4);
 
            [self updateInteractiveTransition:fraction];
            break;
        }
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            // 3. Gesture over. Check if the transition should happen or not
            self.interacting = NO;
            if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
                [self cancelInteractiveTransition];
            } else {
                [self finishInteractiveTransition];
            }
            break;
        }
        default:
            break;
    }
}

以上部分很简单而且并不是我们所关心的部分(虽然名字有点难记忆,但是不难理解)。完成以上代码,就可以实现滑动返回+右进右出的present样式了。

//还有一点需要注意的是,需要禁止全屏手势在导航控制器栈里有多个元素时响应
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    if ([self.presentingVC isKindOfClass:[BaseNavigationController class]]) {
        BaseNavigationController *nav = (BaseNavigationController*)self.presentingVC;
        if (nav.viewControllers.count >=2) {
            return NO;
        }
    }
    return YES;
}

四、遇到的问题

我们(或者说大多数APP)都有这样的需求,当收到推送时需要从底部弹出一个ViewController。 但是因为我们是present 出来了一些页面,当收到推送的时候我们并不能一下子知道从哪个ViewController 上present 出来一个需要用户响应的控制器。比方说 HomeViewController ---(右进右出present)---->ReportMessageViewController后,从代码的角度我们不能一下子拿到展现在用户面前的控制器(因为你不知道是否已经present了,也不知道谁调用的present)。因此我遇到了第一个问题(很多的项目可以通过tab、nav 来确定,而且present 出控制器的场景比较固定):

1、到底谁是当前正在响应的控制器?

要想解决这个问题我们首先要知道响应者链条。即当我们点击了屏幕上的一个按钮,事件是怎么传递的。UIView 和 UIViewController 都是继承自UIResponder的,他们都可以成为响应者。因此我们只要遍历屏幕上最上方的那些View(叶子节点),纵向循环找到他们的nextResponder,直至找到为UIViewController类的响应者。

// 获取某个view 的叶子 View(一般为Window)
/*
 
*/
+(NSMutableArray *)getTopSubViewsWithParentView:(UIView *)rootView{
    NSMutableArray *stack = [NSMutableArray array];
    NSMutableArray *leafNodes = [NSMutableArray array];//存放叶子节点
    if (rootView.subviews.count == 0) {
        return nil;
    }
    [stack addObjectsFromArray:rootView.subviews];//把根视图的所有第一层子视图入栈
    while (stack.count != 0) {
        UIView *subView = [stack lastObject];//取出顶部元素并判断是否为叶子节点
        [stack removeLastObject];
        if (subView.subviews.count != 0) {//不是叶子节点的话将其子视图继续入栈
            [stack addObjectsFromArray:subView.subviews];
        }else{
            [leafNodes addObject:subView];//如果是叶子节点则将其入栈(叶子节点的栈)
        }
    }
    return leafNodes;
}
 
//获取某个视图在哪个控制器上
+(UIViewController*)getViewControllerWithView:(UIView*)view{
     
    UIResponder *res = view;
    while (res) {
        if (res.nextResponder) {
            res = res.nextResponder;
        }
        if ([res isKindOfClass:[UIViewController class]]) {
             
            UIViewController *vc = (UIViewController*)res;
            return vc;
        }
    }
    return nil;
}
 
//这段代码的意思是,如果我能判断的更精确就精确些。比如某个导航控制器,你说他在响应也行,他的top元素在响应也行,显然我想精确到top元素
+(UIViewController*)getCurrentVC{
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
     
    NSMutableArray *array = [self getTopSubViewsWithParentView:keyWindow];
    UINavigationController *nav = nil;
    UITabBarController *tab = nil;
    for (UIView *subView in array) {
         
        UIViewController *vc = [self getViewControllerWithView:subView];
        if (!([vc isKindOfClass:[UINavigationController class]] || [vc isKindOfClass:[UITabBarController class]])) {
            return vc;
        }
        if ([vc isKindOfClass:[UINavigationController class]]) {
            nav = (UINavigationController*)vc;
        }
        if ([vc isKindOfClass:[UITabBarController class]]) {
            tab = (UITabBarController *)vc;
        }
    }
    if (nav) {
        return nav;
    }
    if (tab) {
        return tab;
    }
    return nil;
}

有了这些代码,问题一就解决了。

2、全屏滑动遇到了可以左右滑动的ScrollView怎么办?

首先说明一下,这个问题我解决的并不完美,因为去除了bounces 效果。因为滑动ScrollView时,ScrollView 的pan 手势会优先响应,并阻止其他手势响应。首先我们先看一下手势代理,看看都有哪些方法:

 1 @protocol UIGestureRecognizerDelegate <NSObject>
 2 @optional
 3 // called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
 4 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
 5 
 6 // called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
 7 // return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
 8 //
 9 // note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
10 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;//手势1、手势2 是否可以共存,即两者都响应,收到事件继续传递下去
11 
12 // called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
13 // return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
14 //
15 // note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
16 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);//在other 响应的情况下,自己是否不响应
17 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
18 
19 // called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
20 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
21 
22 // called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press
23 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

通过打印scrollView 的pan手势我们会发现 pan手势的代理是scrollView ,我尝试过改变pan 的delegate 但是发现会崩溃,apple 是不允许改变这个delegate 的。所以我想到了写一个UIScrollView的子类。

//
//  PanScrollView.m
//  Property
//
//  Created by 高雅馨on 16/7/25.
//  Copyright © 2016年  高雅❤️. All rights reserved.
//
 
#import "PanScrollView.h"
 
@implementation PanScrollView
 
-(instancetype)init{
    if (self = [super init]) {
        self.bounces = NO;
    }
    return self;
}
 
/*
 是否将相应传递给other
  
  
 当偏移量X值为0的时候全屏手势和pan 手势同时响应。全屏手势向右滑动时 ,由于bounces效果已经被去掉了,所以偏移量不变,ViewController 只有dismiss 效果。当向左滑动时全屏手势没有任何效果,只要scroll效果。
 */
 
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
     
    if (gestureRecognizer == self.panGestureRecognizer  && otherGestureRecognizer == self.otherGes){
        if (self.contentOffset.x==0) {
            return YES;
        }
    }
    return NO;
}
 
@end

至此,大部分可见的问题解决了,可能并不完美,也不适合,但是尝试也是一种美好的回忆,希望能对大家有帮助。

上一篇 下一篇

猜你喜欢

热点阅读