2019.5.29 修改Method Swizzling部分内容
优雅地开启全屏侧滑手势。
一、使用 作者给UINavigationController和UIViewController都添加了分类,并进行了默认的参数设置,因此不做任何配置就能拥有这个功能。
二、原理 作者通过方法交换,hook到系统原生push方法中的手势中的target和动画调用selector,创建自己的UIPanGestureRecognizer,并设置它的target和selector。
三、接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @interface UINavigationController (FDFullscreenPopGesture)@property (nonatomic, strong, readonly) UIPanGestureRecognizer *fd_fullscreenPopGestureRecognizer;@property (nonatomic, assign) BOOL fd_viewControllerBasedNavigationBarAppearanceEnabled;@end @interface UIViewController (FDFullscreenPopGesture)@property (nonatomic, assign) BOOL fd_interactivePopDisabled;@property (nonatomic, assign) BOOL fd_prefersNavigationBarHidden;@property (nonatomic, assign) CGFloat fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;@end
四、源码阅读 _FDFullscreenPopGestureRecognizerDelegate _FDFullscreenPopGestureRecognizerDelegate
对象。遵循UIGestureRecognizerDelegate
协议,主要用于决定控制器是否能响应手势。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @interface _FDFullscreenPopGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate >@property (nonatomic , weak ) UINavigationController *navigationController;@end @implementation _FDFullscreenPopGestureRecognizerDelegate - (BOOL )gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer { if (self .navigationController.viewControllers.count <= 1 ) { return NO ; } UIViewController *topViewController = self .navigationController.viewControllers.lastObject; if (topViewController.fd_interactivePopDisabled) { return NO ; } CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view]; CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge; if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) { return NO ; } if ([[self .navigationController valueForKey:@"_isTransitioning" ] boolValue]) { return NO ; } CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view]; BOOL isLeftToRight = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ; CGFloat multiplier = isLeftToRight ? 1 : - 1 ; if ((translation.x * multiplier) <= 0 ) { return NO ; } return YES ; } @end
UIViewController (FDFullscreenPopGesturePrivate) UIViewController的分类。给其添加关联属性fd_willAppearInjectBlock
。hook viewWillAppear
方法并在其中调用fd_willAppearInjectBlock
回调,hook viewWillDisappear
方法,并在其中根据控制器的fd_prefersNavigationBarHidden
属性来设置状态栏的显示与否。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);@interface UIViewController (FDFullscreenPopGesturePrivate )@property (nonatomic , copy ) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;@end @implementation UIViewController (FDFullscreenPopGesturePrivate )+ (void )load { static dispatch_once_t onceToken; dispatch_once (&onceToken, ^{ Method viewWillAppear_originalMethod = class_getInstanceMethod(self , @selector (viewWillAppear:)); Method viewWillAppear_swizzledMethod = class_getInstanceMethod(self , @selector (fd_viewWillAppear:)); method_exchangeImplementations(viewWillAppear_originalMethod, viewWillAppear_swizzledMethod); Method viewWillDisappear_originalMethod = class_getInstanceMethod(self , @selector (viewWillDisappear:)); Method viewWillDisappear_swizzledMethod = class_getInstanceMethod(self , @selector (fd_viewWillDisappear:)); method_exchangeImplementations(viewWillDisappear_originalMethod, viewWillDisappear_swizzledMethod); }); } - (void )fd_viewWillAppear:(BOOL )animated { [self fd_viewWillAppear:animated]; if (self .fd_willAppearInjectBlock) { self .fd_willAppearInjectBlock(self , animated); } } - (void )fd_viewWillDisappear:(BOOL )animated { [self fd_viewWillDisappear:animated]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC )), dispatch_get_main_queue(), ^{ UIViewController *viewController = self .navigationController.viewControllers.lastObject; if (viewController && !viewController.fd_prefersNavigationBarHidden) { [self .navigationController setNavigationBarHidden:NO animated:NO ]; } }); } - (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock { return objc_getAssociatedObject(self , _cmd); } - (void )setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block { objc_setAssociatedObject(self , @selector (fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC); } @end
UINavigationController (FDFullscreenPopGesture) UINavigationController的分类。hookpushViewController:animated:
方法,给响应push手势的view添加自定义的fd_fullscreenPopGestureRecognizer
手势。当然,fd_fullscreenPopGestureRecognizer
的target和selector与push原生手势的target及selector保持一致。除此之外,fd_fullscreenPopGestureRecognizer
手势的代理是上面的_FDFullscreenPopGestureRecognizerDelegate
对象,目的是决定是否响应手势。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 @implementation UINavigationController (FDFullscreenPopGesture )+ (void )load { static dispatch_once_t onceToken; dispatch_once (&onceToken, ^{ Class class = [self class ]; SEL originalSelector = @selector (pushViewController:animated:); SEL swizzledSelector = @selector (fd_pushViewController:animated:); Method originalMethod = class_getInstanceMethod(class , originalSelector); Method swizzledMethod = class_getInstanceMethod(class , swizzledSelector); BOOL success = class_addMethod(class , originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class , swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void )fd_pushViewController:(UIViewController *)viewController animated:(BOOL )animated { if (![self .interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self .fd_fullscreenPopGestureRecognizer]) { [self .interactivePopGestureRecognizer.view addGestureRecognizer:self .fd_fullscreenPopGestureRecognizer]; NSArray *internalTargets = [self .interactivePopGestureRecognizer valueForKey:@"targets" ]; id internalTarget = [internalTargets.firstObject valueForKey:@"target" ]; SEL internalAction = NSSelectorFromString (@"handleNavigationTransition:" ); self .fd_fullscreenPopGestureRecognizer.delegate = self .fd_popGestureRecognizerDelegate; [self .fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction]; self .interactivePopGestureRecognizer.enabled = NO ; } [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController]; if (![self .viewControllers containsObject:viewController]) { [self fd_pushViewController:viewController animated:animated]; } } - (void )fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController { if (!self .fd_viewControllerBasedNavigationBarAppearanceEnabled) { return ; } __weak typeof (self ) weakSelf = self ; _FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) { __strong typeof (weakSelf) strongSelf = weakSelf; if (strongSelf) { [strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated]; } }; appearingViewController.fd_willAppearInjectBlock = block; UIViewController *disappearingViewController = self .viewControllers.lastObject; if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) { disappearingViewController.fd_willAppearInjectBlock = block; } } - (_FDFullscreenPopGestureRecognizerDelegate *)fd_popGestureRecognizerDelegate { _FDFullscreenPopGestureRecognizerDelegate *delegate = objc_getAssociatedObject(self , _cmd); if (!delegate) { delegate = [[_FDFullscreenPopGestureRecognizerDelegate alloc] init]; delegate.navigationController = self ; objc_setAssociatedObject(self , _cmd, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return delegate; } - (UIPanGestureRecognizer *)fd_fullscreenPopGestureRecognizer { UIPanGestureRecognizer *panGestureRecognizer = objc_getAssociatedObject(self , _cmd); if (!panGestureRecognizer) { panGestureRecognizer = [[UIPanGestureRecognizer alloc] init]; panGestureRecognizer.maximumNumberOfTouches = 1 ; objc_setAssociatedObject(self , _cmd, panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return panGestureRecognizer; } - (BOOL )fd_viewControllerBasedNavigationBarAppearanceEnabled { NSNumber *number = objc_getAssociatedObject(self , _cmd); if (number) { return number.boolValue; } self .fd_viewControllerBasedNavigationBarAppearanceEnabled = YES ; return YES ; } - (void )setFd_viewControllerBasedNavigationBarAppearanceEnabled:(BOOL )enabled { SEL key = @selector (fd_viewControllerBasedNavigationBarAppearanceEnabled); objc_setAssociatedObject(self , key, @(enabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
UIViewController (FDFullscreenPopGesture) UIViewController的分类。给其添加关联属性fd_interactivePopDisabled
、fd_interactivePopMaxAllowedInitialDistanceToLeftEdge
和fd_prefersNavigationBarHidden
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @implementation UIViewController (FDFullscreenPopGesture )- (BOOL )fd_interactivePopDisabled { return [objc_getAssociatedObject(self , _cmd) boolValue]; } - (void )setFd_interactivePopDisabled:(BOOL )disabled { objc_setAssociatedObject(self , @selector (fd_interactivePopDisabled), @(disabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL )fd_prefersNavigationBarHidden { return [objc_getAssociatedObject(self , _cmd) boolValue]; } - (void )setFd_prefersNavigationBarHidden:(BOOL )hidden { objc_setAssociatedObject(self , @selector (fd_prefersNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CGFloat )fd_interactivePopMaxAllowedInitialDistanceToLeftEdge { #if CGFLOAT_IS_DOUBLE // CGFLOAT_IS_DOUBLE宏: 64位下是1 否则0, 特别严谨 return [objc_getAssociatedObject(self , _cmd) doubleValue]; #else return [objc_getAssociatedObject(self , _cmd) floatValue]; #endif } - (void )setFd_interactivePopMaxAllowedInitialDistanceToLeftEdge:(CGFloat )distance { SEL key = @selector (fd_interactivePopMaxAllowedInitialDistanceToLeftEdge); objc_setAssociatedObject(self , key, @(MAX(0 , distance)), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
五、再谈Method Swizzling 实例方法交换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @interface ViewController () @end @implementation ViewController - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated] NSLog(@"原始的方法实现" ) } @end @implementation ViewController (MethodSwizzling1) + (void)load { static dispatch_once_t onceToken dispatch_once(&onceToken, ^{ Class cls = [self class] SEL originalSel = @selector(viewWillAppear:) SEL swizzledSel = @selector(ya1_viewWillAppear:) Method originalMethod = class_getInstanceMethod(cls, originalSel); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel); if (class_addMethod(cls, originalSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) { class_replaceMethod(cls, swizzledSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }) } - (void)ya1_viewWillAppear:(BOOL)animated { [self ya1_viewWillAppear:animated] NSLog(@"第一次在分类里面互换" ) } @end
类方法交换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL originalSel = @selector(a) ; SEL swizzledSel = @selector(b) ; Class class = object_getClass (self); Method originalMethod = class_getInstanceMethod(class , originalSel); Method swizzledMethod = class_getInstanceMethod(class , swizzledSel); if (class_addMethod(class , originalSel , method_getImplementation (swizzledMethod), method_getTypeEncoding(swizzledMethod))) { class_replaceMethod(class , swizzledSel , method_getImplementation (originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } + (void)b { NSLog(@"b" ); }
问题1:实例方法交换与类方法交换有什么区别? 没有大的变化,唯一的区别在于获取方法所属的对象上,一个获取的是类对象一个获取的是元类对象。即一个是[self class]
或者说[self self]
,一个是object_getClass(self)
(在类方法load中调用的)。 为什么会有这种区别?原因在于,实例方法存储在类对象中,类方法存储在元类对象中。
再一个,看看class
的实现:
1 2 3 4 5 6 7 + (Class)class { return self ; } - (Class)class { return object_getClass(self ); }
网上许多文章这么说的:
object_getClass与self.class的区别 self.class:当self是实例对象的时候,返回的是类对象,否则则返回自身。 object_getClass:获得的是isa的指向
这个“返回自身”很含糊。
object_getClass()
获取isa指向毋庸置疑。实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向根元类,根元类的isa指向它自己,耳熟能详。
关键是class
,self指向了消息的接收者(“the object that’s received this message”),很自然地,实例方法的消息接收者是实例对象,类方法的消息接收者是类对象。根据代码实现来看,实例方法调用class
是获取实例对象的isa指向,即类对象。而类对象调用class
返回的消息接收者自己,这个“自身”指的就是类对象 。于是,不管是实例对象还是类对象调用class
方法,返回的总是类对象。
同样地,在实例方法的交换中,这几种获取类对象的方式是等价的:
1 2 3 Class class = [self self ];Class class = [self class ];Class class = self ;
调用self
的self
方法也没啥奇怪的,源码是这样的:
1 2 3 4 5 6 7 - (id)self { return self ; } + (Class)class { return self ; }
问题2:能否做到实例方法与类方法交换? 实例方法与实例方法互换、类方法与类方法互换都很容易做到。那一个实例方法与一个类方法互换,或者一个类方法与一个实例方法互换可以做到吗?答案是肯定的。 根据上文讨论,关键在于获取方法所属的对象上,即巧妙控制好获取的类对象和元类对象即可。
(1)实例方法与类方法互换(新的实例方法交换原先的类方法) 1 2 3 4 SEL originalSel = @selector(classMethod);SEL swizzledSel = @selector(newInstanceMethod);Class originClass = object_getClass(self);Class swizzleClass = self;
(2)类方法与实例方法互换(新的类方法交换原先的实例方法) 1 2 3 4 SEL originalSel = @selector(instanceMethod);SEL swizzledSel = @selector(newClassMethod);Class originClass = self;Class swizzleClass = object_getClass(self);
二者的共同实现是这样的:
1 2 3 4 5 6 7 Method originalMethod = class_getInstanceMethod (originClass, originalSel) ;Method swizzledMethod = class_getInstanceMethod (swizzleClass, swizzledSel) ;if (class_addMethod(originClass, originalSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) else
问题3:方法二次交换是否会影响第一次交换(造成第一次交换失效)? 比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @implementation ViewController (MethodSwizzling2) + (void)load { static dispatch_once_t onceToken dispatch_once(&onceToken, ^{ SEL originalSel = @selector(viewWillAppear:) SEL swizzledSel = @selector(ya2_viewWillAppear:) Method originalMethod = class_getInstanceMethod(self, originalSel); Method swizzledMethod = class_getInstanceMethod(self, swizzledSel); if (class_addMethod(self, originalSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) { class_replaceMethod(self, swizzledSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }) } - (void)ya2_viewWillAppear:(BOOL)animated { [self ya2_viewWillAppear:animated] NSLog(@"第二次在分类里面互换" ) } @end
很明显不会。
原因: Compile Sources
中设定了分类的编译顺序为ViewController+MethodSwizzling.m --> ViewController+MethodSwizzling2.m
,load
方法的调用顺序也是这样。最新先调用主类的viewWillAppear:
方法不必多说,接着调用分类ViewController+MethodSwizzling
中的方法互换逻辑,使得第一次方法互换成功。紧接着调用分类ViewController+MethodSwizzling2
中的方法互换逻辑,这个不会影响第一次方法互换的逻辑,相当于在第一次的方法互换之后再互换一次。
第一次: ya_viewWillAppear
的实现与viewWillAppear
的实现互换 第二次: ya2_viewWillAppear
的实现与ya_viewWillAppear
的实现互换,因为viewWillAppear
的实现被ya_viewWillAppear
代替了。所以主类和各个分类的方法都被清晰地调用了。
问题4:为什么需要class_replaceMethod()函数? 1,不单独使用method_exchangeImplementations()的原因? 原先的方法存在(有实现),自不必说,使用method_exchangeImplementations()
直接交换函数指针即可。 但是如果原先的方法不存在,包括本来没有实现父类的方法。只使用method_exchangeImplementations(),会在本类没有实现父类的方法的情况下造成hook错误:自己的新方法跟父类的旧方法互换了,父类方法调用时一旦调用到子类特有的方法就会产生crash。
2,class_addMethod()与class_replaceMethod()配合的原因? 如果本类没有实现父类的方法,class_addMethod()会返回YES,此时旧selector、新selector都指向了新IMP,需要调用class_replaceMethod()把新selector指向旧IMP。
问题5:方法交换的标准姿势为什么是load方法配合dispatch_once? 常与+ (void)load;
方法在一起比较的是+ (void)initialize;
方法。为什么是+ (void)load;
方法?
+ (void)load;
是在该类被加载到Runtime时调用的,在手动实现之后,一定会被调用,且正常情况下只会被调用一次,子类也不会多次调用父类的load方法(因为load方法时通过函数指针直接调用,而普通方法是通过消息机制调用。)。
+ (void)initialize;
是在该类收到第一条消息前被调用,如果不向它发送消息(调用类方法或者实例方法),则该方法不会被调用。如果一个子类没有实现+ (void)initialize;
方法,那么父类的该方法会被调用多次。
希望方法交换的逻辑一定会被执行 ,所以选择了load方法。最根本也最具有说服力的原因是initialize是基于消息机制的,如果在主类的initialize方法中实现了方法交换逻辑,在分类中又实现了initialize方法,由于分类中的方法在元类对象方法列表的前面,所以会造成方法交换逻辑并不会生效。而load方法是通过函数指针直接调用,父类、子类、父类的分类和子类的分类的load方法都会被依次调用,同样的情况,方法交换逻辑却依然会生效。再者,假定在父类的initialize方法中实现了方法交换逻辑,子类会调用父类的initialize方法,如果没有使用dispatch_once
,会造成方法交换逻辑多次执行。而load方法,不需要也不应该调用[super load]
,总而言之就是,“initialize方法会造成方法交换不具备稳定性”。
为什么是dispatch_once
? 正常情况下load方法只会被执行一次,但是要考虑手动调用的情况(一般来说不需要手动调用):[ViewController load];
。使用dispatch_once更加完备地保证只执行一次。
六、自定义侧滑手势 除了全屏侧滑之外,有些情况下需要自定义侧滑手势,这时可以使用UIScreenEdgePanGestureRecognizer
实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 - (void )viewDidLoad { [super viewDidLoad]; UIView *orangeView = [[UIView alloc] initWithFrame:self .view.bounds]; orangeView.backgroundColor = UIColor .orangeColor; [self .view addSubview:orangeView]; UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector (handlePop:)]; pan.edges = UIRectEdgeLeft ; [orangeView addGestureRecognizer:pan]; self .orangeView = orangeView; } - (void )handlePop:(UIScreenEdgePanGestureRecognizer *)pan { void (^setOriginX)(UIView *, CGFloat ) = ^(UIView *view, CGFloat x) { [UIView animateWithDuration:0.15 animations:^{ CGRect frame = view.frame; frame.origin.x = x; view.frame = frame; }]; }; UIView *targetView = pan.view; CGFloat offsetX = [pan translationInView:targetView].x; if (pan.state == UIGestureRecognizerStateChanged ) { targetView.center = CGPointMake (targetView.center.x + offsetX, targetView.center.y); [pan setTranslation:CGPointZero inView:targetView.superview]; } else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled ) { if (targetView.frame.origin.x / targetView.frame.size.width > 0.3 ) { setOriginX(targetView, targetView.bounds.size.width); } else { setOriginX(targetView, 0 ); } } }
七、总结 作为FDFullscreenPopGesture的源码阅读文章,实际上重心却不在它这。拜读下来,其实都是些常见的东西:关联属性,方法交换。其实解决问题最重要的,是思路。不然可能自己写了一大堆代码,也不能很好高效地解决问题。
以FDFullscreenPopGesture为引子,又重点回顾了方法交换,作为拓展,回答了引出的许多问题,像实例方法与类方法交换完全是脑洞来的,工作中我还没这么用过😂😂。最后简单介绍了UIScreenEdgePanGestureRecognizer,这个很好使的。
参考资料一个丝滑的全屏滑动返回手势 iOS利用Runtime自定义控制器POP手势动画 iOS-FDFullscreenPopGesture详解