让 iOS APP 支持全屏返回

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

解决思路如下:
  • UINavigationController的属性中, 系统的返回手势是 interactivePopGestureRecognizer 这个手势所处理的
  • 一旦在某个页面自定义了系统导航栏的返回按钮, interactivePopGestureRecognizer 就会让当前页面的左侧边缘返回手势失效, 但是其他使用系统返回按钮的页面依然可以右滑返回, 所以系统做了特殊处理
  • 而这种情况是由 interactivePopGestureRecognizerdelegate 所导致的
  • 解决方式是只要设置 interactivePopGestureRecognizerdelegatenil, 如果自定义了系统导航栏的返回按钮, 当前页面也可以右滑返回
  • 需要注意的是, 当UINavigationController 只有一个子控制器时 , 这时如果在屏幕左侧边缘触发右滑手势, app 将会卡住无法响应, 这是因为 interactivePopGestureRecognizerdeleagete此时不能为nil , 需要delegate 来处理这种特殊的情况
  • 自定义一个UINavigationController的子类, 在子类内部做特殊处理
代码如下:
#import "NavViewController.h"

@interface NavViewController ()

/** 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 = ; 
target= <(
                    action=handleNavigationTransition:,
                    target=<_UINavigationInteractiveTransition 0x7f9def50c760>
                )>
>
  • 可以发现, interactivePopGestureRecognizer 其实是UIScreenEdgePanGestureRecognizer的一个实例
  • 查看UIScreenEdgePanGestureRecognizer的头文件, 可以发现有一个属性
//
//  UIScreenEdgePanGestureRecognizer.h
//  Copyright (c) 2012-2017 Apple Inc. All rights reserved.
//

#import 
#import 

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: @""
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 () 
@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:
; layer = >
self.view:
; layer = >
  • 可以看到, 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 支持全屏返回)