自定义导航控制器返回按钮和实现全屏滑动返回功能

导航控制器的返回按钮设置以及一些细节处理

  • 导航控制器的返回按钮可以通过下面两个属性来设置
// 1.来源控制器的backBarButtonItem属性来设置
sourceVC.navigationItem.backBarButtonItem

// 2.目标口控制器的leftBarButtonItem属性来设置
destinationVC.navigatinoItem.leftBarButtonItem
  • 根据谁的东西谁管理的原则,我们一般使用的是leftBarButtonItem属性来设置
    • 可以知道leftBarButtonItem是一个UIBarButtonItem类的一个实例.
    • 苹果官方提供的可以创建UIBarButtonItem类的实例的方法一共有下面几种方法
    • 可以快速创建只显示文字(initWithTitle), 只显示图片(initWithImage), 还可以设置为任何自定义的控件(initWithCustomView).
    - (instancetype)init ;
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
    - (instancetype)initWithImage:(nullable UIImage *)image style:(UIBarButtonItemStyle)style target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithImage:(nullable UIImage *)image landscapeImagePhone:(nullable UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithTitle:(nullable NSString *)title style:(UIBarButtonItemStyle)style target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithCustomView:(UIView *)customView;
    ```
- **需求:** 返回按钮在普通状态下和高亮状态下有自己的颜色和状态.
- **解决方法:** 一般会将`UIButton`对象包装成一个`UIBarButtonItem`对象来赋值给`leftBarButtonItem`.
- **问题:**如果直接将`UIButton`设置为`leftBarButtonItem`,系统会将按钮的点击返回扩大,影响用户体验.
- **方法改善:**
    - 1.自定义`UIView`,在里面封装一个`UIButton`,设置按钮的颜色和状态即可
        - **细节需求**: 需要将按钮向左边移动一段距离,这里我们假设为10(因为默认设置的返回按钮的位置和导航控制器最左边的间隙有点大)
        - 直接调整`UIView`的`frame`是不可以的,所以这里可以通过调整`UIButton`的`frame`或者`contentEdgeInsets`来达到显示的效果是向左边移动的.这里采用的是在`layoutSubviews`中直接修改`UIButton`的`frame`的`x`值的方法(具体代码如下)
        - **这里又出现一个小问题:**就是当我们设置按钮的x值为负值的时候(按钮相对于父控件是向左边移动的), 超出的父控件的部分是无法点击的.
            - **问题分析:**使用事件传递的知识,首先让我们来复习一下事件传递的过程
            
            ```objc
            1.首先当产生一个事件的时候,应用程序UIApplication会首先接收到这个事件,并将事件分发下去
            2.一般是将事件传递给主窗口,然后会调用view的hitTest:方法使用以下的三个步骤来找到窗口视图层次结构中最合适的view来处理事件
                2.1首先先判断当前的view是否可以接收事件(hidden == NO && userInteractionEnabled == YES && alpha > 0.01 满足这三个条件,表示可以接收事件)
                2.2满足上面条件以后,在判断当前的点是否在view上面
                2.3如果上面两个条件都满足,那么从后向前遍历view.subViews数组,在使用上面三个条件判断,直到找到最合适的view为止
            3.当找到最合适的view之后,会调用最合适的view的监听事件的方法,例如touchesBegan等方法(根据产生的事件不同调用不同的方法.),如果该view没有实现touchesBegan等方法,或者在这个方法中调用了[super touchesBegan], 那么事件会顺着响应者链条向上传递,调用上一个响应者的touchesBegan等方法,依次类推.
            ```
            - 从上面事件传递的过程中,我们可以得知,当点击返回按钮(超出父控件位置)的时候,这个点击事件会先传递给button的父控件, 调用父控件的hitTest:方法来判断button的父控件是否是合适的View.
            - 首先它会判断button的父控件是否可以处理事件(可以)
            - 然后判断点是不是在button的父控件上面,由于这个点的坐标是超出父控件的,所以在这里的判断就不成立,也就不会将事件传递给button了.
            - **解决方法:**当事件传递给Button的父控件的时候,我们在hitTest里面进行判断即可.具体解决代码如下(在`hitTest:`方法中)

      ```objc
      #import "JGBackView.h"
          
      @interface JGBackView ()
      /** 按钮 */
      @property (nonatomic, weak) UIButton *button;
      @end
          
      @implementation JGBackView
      - (instancetype)initWithFrame:(CGRect)frame
      {
          if (self = [super initWithFrame:frame]) {
            
            UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
            
            //imageWithOriginalMode:这个方法是我自己抽的分类,就是设置图片的渲染模式为originalMode
            [backButton setImage:[UIImage imageWithOriginalMode:@"navigationButtonReturn"] forState:UIControlStateNormal];
            [backButton setImage:[UIImage imageWithOriginalMode:@"navigationButtonReturnClick"] forState:UIControlStateHighlighted];
            [backButton setTitle:@"返回" forState:UIControlStateNormal];
            [backButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
            [backButton setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
            [backButton sizeToFit];
            self.button = backButton;
            [self addSubview:backButton];
             
          }
        return self;
      }
          
      // 将UIButton的事件监听方法传递出去
      - (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
      {
        [self.button addTarget:target action:action forControlEvents:controlEvents];
      }
          
      // 这里方法用来处理当按钮超出父控件的部分无法点击的问题
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          // 将点的坐标(这个点的坐标坐标系是button的父控件,在这里就是self)转换成button坐标系上面的点的坐标
          CGPoint btnP = [self convertPoint:point toView:self.button];
          // 判断该点是不是在按钮上面
          if (CGRectContainsPoint(self.button.bounds, btnP)) {
             
          // 如果点在按钮上,那么这个按钮就是最合适的view(会让button来处理事件)
            return self.button;
         }
         
        // 恢复默认做法
        return [super hitTest:point withEvent:event];
      }
          
      - (void)layoutSubviews
      {
         [super layoutSubviews];
         
          // 设置按钮的x值为负值,即像左边移动
          self.button.jg_x -= 10;
             
          // 设置自身的尺寸
          self.jg_width = self.button.jg_width;
          self.jg_height = self.button.jg_height;
             
          // 如果这里设置了x值,那么在返回按钮刚显示的时候,会出现位置不正确的bug,所以这里我们不设置x,采用系统默认的x值就可以了
        self.jg_y = (44 - self.jg_height) * 0.5;
}
@end
    ```

- 全局设置导航栏上返回按钮(设置非根控制器的返回按钮)
    - 在导航控制跳转的时候,一定会调用控制器的`pushViewController:`这个方法,而在这方法中我们又可以拿到目的控制器,而我们推荐使用的就是使用目的控制器的`destinationVC.navigatinoItem.leftBarButtonItem`方法来设置返回按钮.
    - **解决方法:**自定义导航控制器,重写`pushViewController:`,在这个方法中来同意设置返回按钮(代码在`pushViewController:`方法中)

```objc
@interface JGNavigationController () 
@end

@implementation JGNavigationController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{

        if (self.childViewControllers.count != 0) { // 当前控制器为非根控制器的时候我们才需要设置返回按钮
            // 设置返回按钮
            JGBackView *backView = [[JGBackView alloc] init];
            [backView addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
            
            // 在这里统一设置返回按钮
            viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backView];
            viewController.hidesBottomBarWhenPushed = YES;
        }
        
        [super pushViewController:viewController animated:animated];
}

@end

自定义返回按钮的默认屏幕边缘滑动返回功能恢复

  • 当我们自定义了导航控制器的返回按钮以后,会出现一个小问题: 苹果自带的导航控制器的滑动返回功能失效了
  • 首先分析,滑动返回的功能是那个类实现的.猜测应该是导航控制器的功能吧.想到滑动返回,应该想到滑动手势UIPanGestureRecognizer.于是我们在导航控制器UINavigationController中去搜索有关手势的关键字Gesture会发现一个属性interactivePopGestureRecognizer
  • 让我们来看看这个属性的官方解释:
The gesture recognizer responsible for popping the top view controller off the navigation stack.
The navigation controller installs this gesture recognizer on its view and uses it to pop the topmost view controller off the navigation stack. You can use this property to retrieve the gesture recognizer and tie it to the behavior of other gesture recognizers in your user interface. When tying your gesture recognizers together, make sure they recognize their gestures simultaneously to ensure that your gesture recognizers are given a chance to handle the event.

这个手势是用来将栈顶控制器移除导航栈的.
导航控制器给它的view添加了这个手势, 用它来将栈顶的控制器移出导航栈.你可以使用这个属性来获取到这个手势对象,也可以将这个手势和你用户界面中的其他手势绑定在一起使用.当你将这些手势混合使用的时候,记得要调用手势的代理方法来允许可以识别多个手势来保证你的手势可以执行它指定的方法.(英语水平有限,只能翻译大概意思)
  • 从以上信息我们可以知道,确实就是这个手势实现的滑动返回功能.
  • 当自定义了滑动返回按钮之后,在viewDidLoad中来打印这个属性来看一看,这个手势是否还存在
; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ff290712d00>)>>
  • 从打印结果可以知道,这个手势是存在的,但是为什么手势的功能没有实现呢?
  • 或者可以这样思考,如何来控制是否识别手势? 这么一想,就想到了手势的代理.手势的代理属性可以监听手势的触发,也可以用来控制是否识别手势的行为.(英语水平有限,只能翻译大概意思)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
  • 想到这里我们猜测,如果使用的返回按钮不是系统自带的类型,那么这个代理方法中就会返回NO.
  • 解决方法测试:将手势的代理属性清空即可,就不会调用手势的代理方法,来禁止识别当前的手势.
  • 问题:当界面处于跟控制器的时候,也有可能会触发滑动返回的手势, 也会调用栈顶控制器的popViewController来将当前的控制器移出栈,由于该控制器为导航控制器的根控制器,所以就会发生异常,这里会产生假死.
  • 解决方法分析:当导航控制器的子控制器为非根控制器的时候,才可以识别手势.所以我们需要在代理方法中来判断什么时候让手势工作.这样我们就需要设置一个代理.
  • 解决方案:设置当前的自定义导航控制器为interactivePopGestureRecognizer这个手势的代理,然后在代理方法中,判断当前的控制器是否为根控制器,如果是根控制器,返回NO, 否则返回YES.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
// 这里我们根据当前导航控制器的子控制数量来判断当前的控制器是否为根控制器
    return self.childViewControllers.count > 1;
}
  • 这样我们就实现了和苹果一样的滑动屏幕边缘的时候,会有返回功能

自定义返回按钮全屏滑动返回功能实现

  • 需求:进一步提升需求,我们需要实现全屏滑动返回的功能

  • 分析:为什么苹果的手势只能实现边缘滑动返回的功能.

    • 1.首先手势的触发,跟手势这个类是有关系的,从这个手势对象的输出信息分析UIScreenEdgePanGestureRecognizer, 这个手势估计就是只有边缘的滑动才会触发的.所以根本原因还是手势类型的问题.
    • 2.其次滑动返回功能的实现是调用了targetaction方法.
    • 功能实现猜想:所以这里我们可以尝试使用一个UIPanGestureRecognizer这个类(苹果提供的全屏幕范围都可以触发手势).来调用系统手势中的targetaction方法.
    • 但是如何拿到targetaction呢.从系统的手势打印信息我们可以得知action的方法名为handleNavigationTransition:, target这个对象的类型为_UINavigationInteractiveTransition所以我们只需要创建一个_UINavigationInteractiveTransition这个类的实例对象,就可以调用这个对象的方法handleNavigationTransition:来实现滑动返回功能.但问题是我们无法创建_UINavigationInteractiveTransition这种类的对象,因为它是苹果私有的.但是我们可以拿到现成的这个类的一个实例对象self.interactivePopGestureRecognizer.delegate,即导航控制器这个手势的默认代理属性就是一个_UINavigationInteractiveTransition这个类的对象.来看一下它的输出信息:
    <_UINavigationInteractiveTransition: 0x7f9fd8d1f9d0>
    
  • 这样就万事俱备了.

  • 最终解决方案:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
        // 首先要静止系统自带的屏幕边缘滑动返回手势
    self.interactivePopGestureRecognizer.enabled = NO;
    // 1.自定义全屏滑动手势
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self.interactivePopGestureRecognizer.delegate action:@selector(handleNavigationTransition:)];
    // 2.设置代理,来判断当栈顶控制器跟控制器的时候,禁止识别手势
    pan.delegate = self;
    [self.view addGestureRecognizer:pan];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    // 判断当前的控制器是否为根控制器
    return self.childViewControllers.count > 1;
}

你可能感兴趣的:(自定义导航控制器返回按钮和实现全屏滑动返回功能)