iOS7以後,導航控制器,自帶了從屏幕左邊緣右滑返回的手勢功能。 但是,如果自定義了導航欄返回按鈕,這項功能就失效了,需要自行實現。又如果需要修改手勢觸發範圍,還是需要自行實現。 廣泛應用的一種實現方案是,採用私有變數和Api,完成手勢交互和返回功能,自定義手勢觸發條件和額外功能。 另一種實現方案是 ...
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項目已更新:[email protected]:ALongWay/base.git