• App开发流程之右滑返回手势功能


    iOS7以后,导航控制器,自带了从屏幕左边缘右滑返回的手势功能。

    但是,如果自定义了导航栏返回按钮,这项功能就失效了,需要自行实现。又如果需要修改手势触发范围,还是需要自行实现。

    广泛应用的一种实现方案是,采用私有变量和Api,完成手势交互和返回功能,自定义手势触发条件和额外功能。

    另一种实现方案是,采用UINavigationController的代理方法实现交互和动画:

    - (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController

                              interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);

     

    - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController

                                       animationControllerForOperation:(UINavigationControllerOperation)operation

                                                    fromViewController:(UIViewController *)fromVC

                                                      toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

    前者,特点是便捷,但是只能使用系统定义的交互和动画;后者,特点是高度自定义,但是需要额外实现交互协议和动画协议。

     

    采用私有变量和Api,实现右滑返回手势功能。

    先看最核心的逻辑:

    - (void)base_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
        if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.base_panGestureRecognizer]) {
            [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.base_panGestureRecognizer];
            
            //使用KVC获取私有变量和Api,实现系统原生的pop手势效果
            NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
            id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
            SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
            self.base_panGestureRecognizer.delegate = [self base_panGestureRecognizerDelegateObject];
            [self.base_panGestureRecognizer addTarget:internalTarget action:internalAction];
            
            self.interactivePopGestureRecognizer.enabled = NO;
        }
        
        [self base_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
        
        if (![self.viewControllers containsObject:viewController]) {
            [self base_pushViewController:viewController animated:animated];
        }
    }

    UINavigationController的interactivePopGestureRecognizer属性,是系统专用于将viewController弹出导航栈的手势识别对象,有一个私有变量名为“targets”,类似为NSArray;该数组第一个元素对象,有一个私有变量名为“target”,即为实现预期交互的对象;该对象有一个私有方法名为“handleNavigationTransition:”,即为目标方法。

    在返回手势交互的UIView(self.interactivePopGestureRecognizer.view)上添加一个自定义的UIPanGestureRecognizer,利用其delegate对象实现的代理方法gestureRecognizerShouldBegin来控制手势生效条件。最后禁用系统的返回手势识别对象,就可以用自定义实现的pan手势来调用系统的pop交互和动画。

    判断pan手势是否能触发返回操作的代码如下:

    -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
    {
        //正在转场
        if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
            return NO;
        }
        
        //在导航控制器的根控制器界面
        if (self.navigationController.viewControllers.count <= 1) {
            return NO;
        }
        
        UIViewController *popedController = [self.navigationController.viewControllers lastObject];
        
        if (popedController.base_popGestureDisabled) {
            return NO;
        }
        
        //满足有效手势范围
        CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
        CGFloat popGestureEffectiveDistanceFromLeftEdge = popedController.base_popGestureEffectiveDistanceFromLeftEdge;
        
        if (popGestureEffectiveDistanceFromLeftEdge > 0
            && beginningLocation.x > popGestureEffectiveDistanceFromLeftEdge) {
            return NO;
        }
        
        //右滑手势
        UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint transition = [panGesture translationInView:panGesture.view];
        
        if (transition.x <= 0) {
            return NO;
        }
        
        return YES;
    }

    其中navigationController还使用了私有变量“_isTransitioning”,用于判断交互是否正在进行中。

    为了使过场动画过程中,导航栏的交互动画自然,需要在UIViewController的viewWillAppear方法中,通过swizzle方法调用导航栏显示或隐藏的动画方法,所以需要增加一个延迟执行的代码块:

    - (void)base_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
    {
        //如果navigationController不显示导航栏,直接return
        if (self.navigationBarHidden) {
            return;
        }
        
        __weak typeof(self) weakSelf = self;
        ViewControllerViewWillAppearDelayBlock block = ^(UIViewController *viewController, BOOL animated) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            if (strongSelf) {
                [strongSelf setNavigationBarHidden:viewController.base_currentNavigationBarHidden animated:animated];
            }
        };
        
        appearingViewController.viewWillAppearDelayBlock = block;
        UIViewController *disappearingViewController = self.viewControllers.lastObject;
        if (disappearingViewController && !disappearingViewController.viewWillAppearDelayBlock) {
            disappearingViewController.viewWillAppearDelayBlock = block;
        }
    }

    实现完整的分类的过程中,使用了一些运行时类型和方法。

    1.引用头文件#import <objc/runtime.h>

    2.为分类增加属性,涉及到了如下方法:

    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

    id objc_getAssociatedObject(id object, const void *key)

    例如UIViewController (PopGesture)中增加的属性base_currentNavigationBarHidden的get/set方法:

    - (BOOL)base_currentNavigationBarHidden
    {
        NSNumber *number = objc_getAssociatedObject(self, _cmd);
        if (number) {
            return number.boolValue;
        }
        
        self.base_currentNavigationBarHidden = NO;
        return NO;
    }
    
    - (void)setBase_currentNavigationBarHidden:(BOOL)hidden
    {
        self.canUseViewWillAppearDelayBlock = YES;
        
        objc_setAssociatedObject(self, @selector(base_currentNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    第一个参数一般为self。

    第二个参数const void *key,要求传入一个地址。

    可以声明一个static char *key;或者static NSString *key;,赋值与否并不重要,因为需要的只是地址,参数为&key。

    而上述代码中使用了_cmd和@selector,作用是一样的。_cmd返回的是当前方法的SEL,@selector也是返回目标方法的SEL,即是函数地址。

    第三个参数即是关联的值。

    第四个参数policy,为枚举类型,基本对应属性引用相关的关键字:

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    
        OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
    
                                                *   The association is not made atomically. */
    
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
    
                                                *   The association is not made atomically. */
    
        OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
    
                                                *   The association is made atomically. */
    
        OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
    
                                                *   The association is made atomically. */
    
    };

    3.理解其他的一些运行时类型或方法

    typedef struct objc_method *Method;//An opaque type that represents a method in a class definition.

    Method class_getInstanceMethod(Class cls, SEL name) //返回实例方法

    Method class_getClassMethod(Class cls, SEL name) //返回类方法

    IMP method_getImplementation(Method m) //Returns the implementation of a method.

    const char *method_getTypeEncoding(Method m) //Returns a string describing a method's parameter and return types.

    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) //Adds a new method to a class with a given name and implementation.

    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) //Replaces the implementation of a method for a given class.

    void method_exchangeImplementations(Method m1, Method m2) //Exchanges the implementations of two methods.

     

    以上方法,可以达到swizzle方法的目的,将分类中新增方法与已有旧的方法交换函数地址,可以作为完全替换(因为运行时,执行的方法名称仍然为viewWillAppear:,但是指向新增方法地址),也可以在新增方法代码中调用当前的方法名称(交换后,当前的方法名称指向旧方法地址)。例如UIViewController中的下列代码:

    +(void)load
    {
        __weak typeof(self) weakSelf = self;
        
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            [weakSelf swizzleOriginalSelector:@selector(viewWillAppear:) withNewSelector:@selector(base_viewWillAppear:)];
            
            [weakSelf swizzleOriginalSelector:@selector(viewDidDisappear:) withNewSelector:@selector(base_viewDidDisappear:)];
        });
    }
    
    +(void)swizzleOriginalSelector:(SEL)originalSelector withNewSelector:(SEL)newSelector
    {
        Class selfClass = [self class];
        
        Method originalMethod = class_getInstanceMethod(selfClass, originalSelector);
        Method newMethod = class_getInstanceMethod(selfClass, newSelector);
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP newIMP = method_getImplementation(newMethod);
        
        //先用新的IMP加到原始SEL中
        BOOL addSuccess = class_addMethod(selfClass, originalSelector, newIMP, method_getTypeEncoding(newMethod));
        if (addSuccess) {
            class_replaceMethod(selfClass, newSelector, originalIMP, method_getTypeEncoding(originalMethod));
        }else{
            method_exchangeImplementations(originalMethod, newMethod);
        }
    }
    
    -(void)base_viewWillAppear:(BOOL)animated
    {
        [self base_viewWillAppear:animated];
        
        if (self.canUseViewWillAppearDelayBlock
            && self.viewWillAppearDelayBlock) {
            self.viewWillAppearDelayBlock(self, animated);
        }
        
        if (self.transitionCoordinator) {
            [self.transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
                if ([context isCancelled]) {
                    self.base_isBeingPoped = NO;
                }
            }];
        }
    }

    特别说明:

    A.+load静态方法将在此分类加入运行时调用(Invoked whenever a class or category is added to the Objective-C runtime),执行顺序在该类自己的+load方法之后。

    B.如果在使用中未明确设置base_currentNavigationBarHidden,canUseViewWillAppearDelayBlock则为NO,因为我封装的父类中提供了类似功能,所以不需要开启分类中同样的功能。该功能目的是提供给直接集成分类的朋友。

    C.UIViewController的transitionCoordinator属性,在当前界面有过场交互时候,该属性有值。并且在交互结束时候,可以回调一个block,以告知过场交互的状态和相关属性。这里声明了一个属性base_isBeingPoped,用于标记当前视图控制器是否正在被pop出导航栈,如果交互取消了,置为NO,最终可以在viewDidDisappear:方法中判断并执行一些操作。

     

    使用UINavigationController的代理方法来实现高度自定义的方案,下次再更新记录。

    =================

    已更新:http://www.cnblogs.com/ALongWay/p/5896982.html

     

    base项目已更新:git@github.com:ALongWay/base.git

  • 相关阅读:
    责任链模式(Chain of Responsibility)
    模板模式(Template Method)
    组合模式(Composite Pattern)
    原型模式(Prototype Pattern)
    策略模式(Strategy Pattern)
    状态模式(State Pattern)
    增删改查
    安卓sql
    安卓第三次作业
    安卓第四周作业
  • 原文地址:https://www.cnblogs.com/ALongWay/p/5893515.html
Copyright © 2020-2023  润新知