美文网首页iOS开发(OC)iOS Learning
让 iOS APP 支持全屏返回

让 iOS APP 支持全屏返回

作者: codingchou | 来源:发表于2018-04-16 18:34 被阅读5次

    场景一 : 如果自定义了系统导航栏的返回按钮, 系统的左侧边缘返回手势将会失效

    解决思路如下:
    • UINavigationController的属性中, 系统的返回手势是 interactivePopGestureRecognizer 这个手势所处理的
    • 一旦在某个页面自定义了系统导航栏的返回按钮, interactivePopGestureRecognizer 就会让当前页面的左侧边缘返回手势失效, 但是其他使用系统返回按钮的页面依然可以右滑返回, 所以系统做了特殊处理
    • 而这种情况是由 interactivePopGestureRecognizerdelegate 所导致的
    • 解决方式是只要设置 interactivePopGestureRecognizerdelegatenil, 如果自定义了系统导航栏的返回按钮, 当前页面也可以右滑返回
    • 需要注意的是, 当UINavigationController 只有一个子控制器时 , 这时如果在屏幕左侧边缘触发右滑手势, app 将会卡住无法响应, 这是因为 interactivePopGestureRecognizerdeleagete此时不能为nil , 需要delegate 来处理这种特殊的情况
    • 自定义一个UINavigationController的子类, 在子类内部做特殊处理
    代码如下:
    #import "NavViewController.h"
    
    @interface NavViewController ()<UINavigationControllerDelegate>
    
    /** tz_PopDelegate */
    @property (nonatomic, strong) id tz_PopDelegate;
    
    @end
    
    @implementation NavViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //保存 interactivePopGestureRecognizer 的delegate
        self.tz_PopDelegate = self.interactivePopGestureRecognizer.delegate;
        self.delegate = self;   
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    #pragma mark - UINavigationControllerDelegate
    
    - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
        NSLog(@"控制器个数: %ld",self.childViewControllers.count);
            self.interactivePopGestureRecognizer.enabled = YES;
        if (viewController == self.viewControllers[0]) {  //如果是在首页,设置interactivePopGestureRecognizer的delegate
            self.interactivePopGestureRecognizer.delegate = self.tz_PopDelegate; // 不支持侧滑
        } else { //如果是二级页面, 设置 interactivePopGestureRecognizer的代理为nil, 支持侧滑
            self.interactivePopGestureRecognizer.delegate = nil; // 支持侧滑
        }
    }
    
    @end
    

    场景二 : 系统默认的侧滑返回手势, 只能在左侧边缘右滑触发, 而现在很多app 都有全屏返回这个需求

    解决方法:
    • 使用Runtime+KVC 分析 UINavigationControllerinteractivePopGestureRecognizer 属性, 打印 interactivePopGestureRecognizer这个对象
    #import "NavViewController2.h"
    
    @interface NavViewController2 ()
    
    @end
    
    @implementation NavViewController2
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"手势 : \n%@", self.interactivePopGestureRecognizer);
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    @end
    
    • 打印结果
    手势 : 
    <
    UIScreenEdgePanGestureRecognizer: 0x7f9def50ee30;
    state = Possible; 
    delaysTouchesBegan = YES; 
    view = <UILayoutContainerView 0x7f9def602b10>; 
    target= <(
                        action=handleNavigationTransition:,
                        target=<_UINavigationInteractiveTransition 0x7f9def50c760>
                    )>
    >
    
    • 可以发现, interactivePopGestureRecognizer 其实是UIScreenEdgePanGestureRecognizer的一个实例
    • 查看UIScreenEdgePanGestureRecognizer的头文件, 可以发现有一个属性
    //
    //  UIScreenEdgePanGestureRecognizer.h
    //  Copyright (c) 2012-2017 Apple Inc. All rights reserved.
    //
    
    #import <UIKit/UIGeometry.h>
    #import <UIKit/UIPanGestureRecognizer.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    /*! This subclass of UIPanGestureRecognizer only recognizes if the user slides their finger
        in from the bezel on the specified edge. */
    NS_CLASS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED @interface UIScreenEdgePanGestureRecognizer : UIPanGestureRecognizer
    @property (readwrite, nonatomic, assign) UIRectEdge edges; //< The edges on which this gesture recognizes, relative to the current interface orientation
    @end
    
    NS_ASSUME_NONNULL_END
    
    • 但是直接修改edges这个属性并不能扩大手势响应范围
    • 上面的打印结果中, 有个属性是我们所需要的
    target= <(
                        action=handleNavigationTransition:,
                        target=<_UINavigationInteractiveTransition 0x7f9def50c760>
                    )>
    
    • 但是直接通过KVC获取这个属性程序会崩溃, 因为interactivePopGestureRecognizer这个对象中并不存在target这个属性
    • 通过Runtime打印interactivePopGestureRecognizer的所有属性, 结果如下:
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"手势 : %@", self.interactivePopGestureRecognizer);
        
        unsigned int count = 0;
        Ivar *var = class_copyIvarList([UIGestureRecognizer class], &count);
        for (int i = 0; i < count; i++) {
            Ivar aVar = *(var + i);
            NSLog(@"成员变量类型%d: %s", i,ivar_getTypeEncoding(aVar));
            NSLog(@"成员变量名%d: %s",i,ivar_getName(aVar));
        }
    }
    
    2018-04-17 10:58:53.095436+0800 ATest[1487:44665] 成员变量名0: _gestureFlags
    2018-04-17 10:58:53.095597+0800 ATest[1487:44665] 成员变量类型1: @"NSMutableArray"
    2018-04-17 10:58:53.095835+0800 ATest[1487:44665] 成员变量名1: _targets
    2018-04-17 10:58:53.095989+0800 ATest[1487:44665] 成员变量类型2: @"NSMutableArray"
    2018-04-17 10:58:53.096124+0800 ATest[1487:44665] 成员变量名2: _delayedTouches
    2018-04-17 10:58:53.096228+0800 ATest[1487:44665] 成员变量类型3: @"NSMutableArray"
    2018-04-17 10:58:53.096341+0800 ATest[1487:44665] 成员变量名3: _delayedPresses
    2018-04-17 10:58:53.096466+0800 ATest[1487:44665] 成员变量类型4: @"UIView"
    2018-04-17 10:58:53.096892+0800 ATest[1487:44665] 成员变量名4: _view
    2018-04-17 10:58:53.097113+0800 ATest[1487:44665] 成员变量类型5: d
    2018-04-17 10:58:53.097449+0800 ATest[1487:44665] 成员变量名5: _lastTouchTimestamp
    2018-04-17 10:58:53.097763+0800 ATest[1487:44665] 成员变量类型6: d
    2018-04-17 10:58:53.098047+0800 ATest[1487:44665] 成员变量名6: _firstEventTimestamp
    2018-04-17 10:58:53.098333+0800 ATest[1487:44665] 成员变量类型7: q
    2018-04-17 10:58:53.098614+0800 ATest[1487:44665] 成员变量名7: _state
    2018-04-17 10:58:53.098872+0800 ATest[1487:44665] 成员变量类型8: q
    2018-04-17 10:58:53.099156+0800 ATest[1487:44665] 成员变量名8: _allowedTouchTypes
    2018-04-17 10:58:53.099388+0800 ATest[1487:44665] 成员变量类型9: q
    2018-04-17 10:58:53.099604+0800 ATest[1487:44665] 成员变量名9: _initialTouchType
    2018-04-17 10:58:53.099885+0800 ATest[1487:44665] 成员变量类型10: @"NSMutableSet"
    2018-04-17 10:58:53.100095+0800 ATest[1487:44665] 成员变量名10: _internalActiveTouches
    2018-04-17 10:58:53.100408+0800 ATest[1487:44665] 成员变量类型11: @"_UIForceLevelClassifier"
    2018-04-17 10:58:53.100660+0800 ATest[1487:44665] 成员变量名11: _forceClassifier
    2018-04-17 10:58:53.100925+0800 ATest[1487:44665] 成员变量类型12: q
    2018-04-17 10:58:53.101146+0800 ATest[1487:44665] 成员变量名12: _requiredPreviewForceState
    2018-04-17 10:58:53.101982+0800 ATest[1487:44665] 成员变量类型13: @"_UITouchForceObservable"
    2018-04-17 10:58:53.102227+0800 ATest[1487:44665] 成员变量名13: _touchForceObservable
    2018-04-17 10:58:53.102449+0800 ATest[1487:44665] 成员变量类型14: @"NSObservation"
    2018-04-17 10:58:53.102747+0800 ATest[1487:44665] 成员变量名14: _touchForceObservableAndClassifierObservation
    2018-04-17 10:58:53.103067+0800 ATest[1487:44665] 成员变量类型15: @"NSMutableArray"
    2018-04-17 10:58:53.103271+0800 ATest[1487:44665] 成员变量名15: _forceTargets
    2018-04-17 10:58:53.103465+0800 ATest[1487:44665] 成员变量类型16: Q
    2018-04-17 10:58:53.103784+0800 ATest[1487:44665] 成员变量名16: _forcePressCount
    2018-04-17 10:58:53.104055+0800 ATest[1487:44665] 成员变量类型17: @"NSObservationSource"
    2018-04-17 10:58:53.104353+0800 ATest[1487:44665] 成员变量名17: _beganObservable
    2018-04-17 10:58:53.104579+0800 ATest[1487:44665] 成员变量类型18: @"NSMutableSet"
    2018-04-17 10:58:53.104839+0800 ATest[1487:44665] 成员变量名18: _failureRequirements
    2018-04-17 10:58:53.105092+0800 ATest[1487:44665] 成员变量类型19: @"NSMutableSet"
    2018-04-17 10:58:53.105312+0800 ATest[1487:44665] 成员变量名19: _failureDependents
    2018-04-17 10:58:53.105589+0800 ATest[1487:44665] 成员变量类型20: @"NSMutableSet"
    2018-04-17 10:58:53.105847+0800 ATest[1487:44665] 成员变量名20: _activeEvents
    2018-04-17 10:58:53.106136+0800 ATest[1487:44665] 成员变量类型21: B
    2018-04-17 10:58:53.106677+0800 ATest[1487:44665] 成员变量名21: _keepTouchesOnContinuation
    2018-04-17 10:58:53.107423+0800 ATest[1487:44665] 成员变量类型22: @"<UIGestureRecognizerDelegate>"
    2018-04-17 10:58:53.107614+0800 ATest[1487:44665] 成员变量名22: _delegate
    2018-04-17 10:58:53.107885+0800 ATest[1487:44665] 成员变量类型23: @"NSArray"
    2018-04-17 10:58:53.108180+0800 ATest[1487:44665] 成员变量名23: _allowedPressTypes
    2018-04-17 10:58:53.108481+0800 ATest[1487:44665] 成员变量类型24: @"NSString"
    2018-04-17 10:58:53.108730+0800 ATest[1487:44665] 成员变量名24: _name
    2018-04-17 10:58:53.109045+0800 ATest[1487:44665] 成员变量类型25: @"UIGestureEnvironment"
    2018-04-17 10:58:53.109347+0800 ATest[1487:44665] 成员变量名25: _gestureEnvironment
    
    • 可以发现, interactivePopGestureRecognizer这个对象中有_targets这个可变数组, 这是我们想要的
    • 打印_targets变量
        //打印_tagets
        NSMutableArray * _targets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        NSLog(@"_targets == %@",_targets);
        NSLog(@"_targets[0] == %@",_targets[0]);
    
     _targets == (
        "(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ffa6ec03560>)"
    )
    2018-04-17 11:13:33.695306+0800 ATest[1617:52185] _targets[0] == (action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ffa6ec03560>)
    
    • 可以发现, _targets这个数组中的元素是 target-action这种格式的, 可以猜测系统的手势中, 是通过一个对象来保存手势的taget 和这个taget对应的action的, 并把这个对象添加到_targets这个数组中保存起来, 通过断点调试得到保存target-action的是UIGestureRecognizerTarget这个私有类
    • 最终解决方法如下:
    @interface NavViewController2 () <UIGestureRecognizerDelegate>
    @property (nonatomic, weak) UIPanGestureRecognizer *popRecognizer;
    
    @end
    
    @implementation NavViewController2
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 注意 : 要让interactivePopGestureRecognizer失效
        self.interactivePopGestureRecognizer.enabled = NO;
        
        //自定义pan手势
        UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] init];
        popRecognizer.delegate = self;
        popRecognizer.maximumNumberOfTouches = 1;
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:popRecognizer];
        
        NSMutableArray *_targets = [self.interactivePopGestureRecognizer valueForKey:@"_targets"];
        
        //获取保存target-action的对象, 是私有类 UIGestureRecognizerTarget 的实例
        id gestureRecognizerTarget = [_targets firstObject];
        
        //获取UIGestureRecognizerTarget中保存的target, 是私有类_UINavigationInteractiveTransition的实例
        id interactiveTransition = [gestureRecognizerTarget valueForKey:@"_target"];
        
        //获取UIGestureRecognizerTarget中保存的action, 是interactiveTransition中响应手势的方法
        SEL handleTransition = NSSelectorFromString(@"handleNavigationTransition:");
        
        //绑定taget-action
        [popRecognizer addTarget:interactiveTransition action:handleTransition];
    }
    
    #pragma mark - UIGestureRecognizerDelegate
    - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
        /**
         *  这里有两个条件不允许手势执行:
         *  1、当前控制器为根控制器;
         *  2、如果这个push、pop动画正在执行(私有属性)
         */
        return self.viewControllers.count != 1 && ![[self valueForKey:@"_isTransitioning"] boolValue];
    }
    
    @end
    
    • 现在, 我们的app就支持丝滑的全屏返回手势了
    • 还可以发现一个有趣的东西, 执行以下打印:
    NSLog(@"\ninteractivePopGestureRecognizer.delegate:\n%@\ninteractiveTransition:\n%@",self.interactivePopGestureRecognizer.delegate, interactiveTransition);
        NSLog(@"-----------------------------------------");
    NSLog(@"\ninteractivePopGestureRecognizer.view:\n%@\nself.view:\n%@",self.interactivePopGestureRecognizer.view, self.view);
    
    2018-04-17 11:59:32.095941+0800 ATest[2222:78230] 
    interactivePopGestureRecognizer.delegate:
    <_UINavigationInteractiveTransition: 0x7fadf3404690>
    interactiveTransition:
    <_UINavigationInteractiveTransition: 0x7fadf3404690>
    2018-04-17 11:59:32.096143+0800 ATest[2222:78230] -----------------------------------------
    2018-04-17 11:59:32.096798+0800 ATest[2222:78230] 
    interactivePopGestureRecognizer.view:
    <UILayoutContainerView: 0x7fadf3602b60; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x60400024f0c0>; layer = <CALayer: 0x60000003c140>>
    self.view:
    <UILayoutContainerView: 0x7fadf3602b60; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x60400024f0c0>; layer = <CALayer: 0x60000003c140>>
    
    • 可以看到, self.interactivePopGestureRecognizer.delegate 跟我们辛辛苦苦才获得的interactiveTransition原来是同个对象, 这样我们可以直接使用self.interactivePopGestureRecognizerdelegate作为自定义手势的target
    • 同样的, self.interactivePopGestureRecognizer.view其实就是UINavigationControllerview, 所以可以对我们上面的viewDidLoad代码做下简化
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 注意 : 要让interactivePopGestureRecognizer失效
        self.interactivePopGestureRecognizer.enabled = NO;
        //自定义pan手势
        UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] init];
        popRecognizer.delegate = self;
        popRecognizer.maximumNumberOfTouches = 1;
        [self.view addGestureRecognizer:popRecognizer];
        
        //获取action
        SEL handleTransition = NSSelectorFromString(@"handleNavigationTransition:");
        //绑定taget-action
        [popRecognizer addTarget:self.interactivePopGestureRecognizer.delegate action:handleTransition];  
    }
    
    运行, 效果是一样的

    相关文章

      网友评论

        本文标题:让 iOS APP 支持全屏返回

        本文链接:https://www.haomeiwen.com/subject/gvwekftx.html