@chinese-ppmt
2018-04-16T05:17:57.000000Z
字数 8218
阅读 1966
runtime
某天,闲来无聊点击微信通讯录右边的索引(具体操作如下:打开微信---通讯录,然后右手点击某个索引,手指别松开,然后左手点击某个联系人cell,跳转后再返回,你刚才长按的放大索引还在,并且再滑动,索引对应字母也不变化,索引视图卡住。),发现了这个bug(我都滑动到L了,放大的索引还是M),如下:

由于自己的项目也有类似的功能,同样操作,也有同样的bug,于是就赶紧修复一下。
问题有了,解决问题的关键就是找到原因并给出合理的解决方案。
Why? 一问
Why? 二问
Why? 三问
原因:放大的索引视图在显示的情况下点击tableViewCell没有隐藏掉.
解决方案:在didSelectRowAtIndexPath方法里隐藏(or移除)放大了的索引视图。方案一:用通知,
didSelectRowAtIndexPath调用的时候给PPSectionTitleIndexView发个通知。
方案二 :用runtime,didSelectRowAtIndexPath调用的时候给UItableView添加一个block,PPSectionTitleIndexView里面的tableView实现block。两种方案对比:
方案一 需要每次调用didSelectRowAtIndexPath时都要发送一个通知;
方案二 什么也不用做。所以,当然方案二更好。
扯了那么多,现在进入实战。
PPSectionTitleIndexView继承自UIView,并声明代理PPSectionTitleIndexViewDelegate,代码如下:
#import <UIKit/UIKit.h>NS_ASSUME_NONNULL_BEGIN@protocol PPSectionTitleIndexViewDelegate <NSObject>@required/**与PPSectionTitleIndexView相关联的view(UITableView)*/-(UITableView *)sectionTitleIndexViewAssociatedView;/**section的titles*/-(NSArray<NSString *> *)sectionTitleIndexViewSectionTitles;@end@interface PPSectionTitleIndexView : UIView/** 索引是否正在显示 */@property(nonatomic,assign,readonly) BOOL isShowingIndex;//这三种不能用于初始化-(instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;-(instancetype)init NS_UNAVAILABLE;+(instancetype)new NS_UNAVAILABLE;-(instancetype)initWithFrame:(CGRect)frame delegate:(_Nullable id<PPSectionTitleIndexViewDelegate>)delegate NS_DESIGNATED_INITIALIZER;+(instancetype)pp_sectionTitleIndexViewWithFrame:(CGRect)frame delegate:(_Nullable id<PPSectionTitleIndexViewDelegate>)delegate;@endNS_ASSUME_NONNULL_END
PPSectionTitleIndexView思路:每个索引用UIButton,当前点击的放大索引用UILabel显示。
核心代码如下:
1.创建UI的
-(instancetype)initWithFrame:(CGRect)frame delegate:(id<PPSectionTitleIndexViewDelegate>)delegate{self = [super initWithFrame:frame];if (self) {self.delegate = delegate;[self creatUI];}return self;}-(void)creatUI{//1. 先检验必须实现的协议有没有实现[self verifyRequiredProtocol];//2. 创建索引UI[self creatIndexUI];//3. 添加平移手势[self addPanGesture];}//此处只贴出来index的button创建代码for (int i = 0; i<self.indexTitles.count; i++) {UIButton *item = [UIButton buttonWithType:UIButtonTypeCustom];[self addSubview:item];item.tag = 100+i;item.frame = CGRectMake(0, topOrBottomMargin+(oneIndexWidth+itemMargin)*i, indexVWidth, oneIndexWidth);item.titleEdgeInsets = UIEdgeInsetsMake(0, self.frame.size.width-oneIndexWidth-5, 0, 5);[item setTitle:self.indexTitles[i] forState:UIControlStateNormal];[item setTitle:self.indexTitles[i] forState:UIControlStateHighlighted];[item setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];[item setTitleColor:[UIColor blueColor] forState:UIControlStateHighlighted];item.backgroundColor = [UIColor clearColor];item.titleLabel.font = [UIFont systemFontOfSize:12];[item addTarget:self action:@selector(btnClickedDown:) forControlEvents:UIControlEventTouchDown]; //只要点击[item addTarget:self action:@selector(btnTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; //点击松手}
2.响应事件的
-(void)btnClickedDown:(UIButton *)sender{//1. 获取当前sectionTitle的frameCGRect currentSectionTitleRect = [self.tableView rectForSection:sender.tag-100];//2. 把tableView当前section滚动到top (不要动画效果更好)[self.tableView setContentOffset:CGPointMake(currentSectionTitleRect.origin.x, currentSectionTitleRect.origin.y) animated:NO];//3. 把当前的index放大显示到屏幕中间NSString *currentIndexStr = sender.titleLabel.text;self.currentIndexShowLB.text = currentIndexStr;//4. 处理indexStrif (currentIndexStr.length == 1) {self.currentIndexShowLB.font = [UIFont systemFontOfSize:35];}else{self.currentIndexShowLB.font = [UIFont systemFontOfSize:18];}//5. 屏幕中间显示当前indexself.currentIndexShowLB.hidden = NO;}-(void)btnTouchUpInside:(UIButton *)sender{//该方法是在btnClickedDown后执行self.currentIndexShowLB.hidden = YES;}#pragma mark --- 添加平移手势-(void)addPanGesture{UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panAction:)];[self addGestureRecognizer:panGR];}-(void)panAction:(UIPanGestureRecognizer *)panGR{//1. 获取手指当前位置的pointCGPoint fingerTapPoint = [panGR locationInView:self];//2. 遍历self上的所有自视图(UIButton *),如果它的tag和上次不一样,执行touchDown方法for (UIView *aV in self.subviews) {if ([aV isKindOfClass:[UIButton class]] && aV.tag != _selectedBtnTag && CGRectContainsPoint(aV.frame, fingerTapPoint)) {_selectedBtnTag = aV.tag;[self btnClickedDown:(UIButton *)aV];}}//3. 手势结束时,隐藏屏幕中间的showIndexLBif (panGR.state == UIGestureRecognizerStateEnded) {self.currentIndexShowLB.hidden = YES;}}
至此,想要的功能算是实现了,但是文章一开始提到的bug也出现了,😜。
关键:tableView的Category
runtime方案的实现步骤如下:
- 拦截UITableView的系统
setDelegate,用pp_setDelegate替换;- 判断delegate是否响应系统的
tableView:didSelectRowAtIndexPath:,如果响应才能继续3;- 根据系统的代理
tableView:didSelectRowAtIndexPath:方法和对应的指针给代理类[delegate class]利用runtime的class_addMethod添加一个didSelected方法@selector(pp_fakeTableView:didSelectRowAtIndexPath:),并替换为pp_tableView:didSelectRowAtIndexPath:.- 利用NSInvocation在替换的didSelected方法
pp_tableView:didSelectRowAtIndexPath:中调用pp_fakeTableView:didSelectRowAtIndexPath:,因为pp_fakeTableView:didSelectRowAtIndexPath:和系统的tableView:didSelectRowAtIndexPath:指针指向相同,所以就相当于调用系统的tableView:didSelectRowAtIndexPath:,这样就实现了我们的需求。
我怕我说明的不清楚,特意画了个图:

代码如下:
///**// * 通过给定的方法名和实现,动态给类添加一个方法// *// * @param cls 要添加方法的类.// * @param name 指定了方法名的要添加到类的方法.// * @param imp 添加方法的函数实现(函数地址).// * @param types 函数的类型,(返回值+参数类型).// *// * @return 如果添加成功就返回YES,失败返回NO(如果该类中已经存在一个相同的方法名的方法实现).// *// * @note 注: class_addMethod将重写父类的实现,但是不会替换该类中已经有的实现,// 如果想改变该类中已有的实现,请使用method_setImplementation// *///OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp,const char * types)-(void)pp_setDelegate:(id<UITableViewDelegate>)delegate{if(delegate){//系统原生的didSelected方法SEL systemDidSelectedSelector = @selector(tableView:didSelectRowAtIndexPath:);//我自己重写的didSelected方法(用于替换系统的)SEL ppDidSelectedSelector = @selector(pp_tableView:didSelectRowAtIndexPath:);//VIP:假的didSelected方法(和系统原生的didSelected方法指针地址相同,但不是一个方法,却响应同样事件)SEL fakeDidSelectedSelector = @selector(pp_fakeTableView:didSelectRowAtIndexPath:);if ([delegate respondsToSelector:systemDidSelectedSelector]) {//系统的代理didSelected方法和对应的指针Method systemDidSelectedMethod = class_getInstanceMethod([delegate class], @selector(tableView:didSelectRowAtIndexPath:));IMP systemDidSelectedMethodIMP = method_getImplementation(systemDidSelectedMethod);//VIP:此处给系统原生的didSelected方法上添加新的方法(只要方法名不一样,就可以成功,详见系统api中的@return说明)//注意此方法放置位置,不能放在下面的class_replaceMethod后面(因为已经被替换了,指针会指向tableView *的pp_tableView:didSelectRowAtIndexPath:)class_addMethod([delegate class], fakeDidSelectedSelector, systemDidSelectedMethodIMP, method_getTypeEncoding(systemDidSelectedMethod));//自己重写的didSelected,用来处理拦截后想做的事情(比如:发通知,block回调等)Method ppDidSelectedMethod = class_getInstanceMethod([self class], ppDidSelectedSelector);IMP ppDidSelectedMethodIMP = method_getImplementation(ppDidSelectedMethod);//用自己重写的替换系统原生的class_replaceMethod([delegate class], systemDidSelectedSelector, ppDidSelectedMethodIMP, method_getTypeEncoding(systemDidSelectedMethod));}}//拦截原生的delegate,别忘了调用(此处调用pp_setDelegate:实际上就是调用setDelegate:,说句不该说的话,此处看不懂,runtime你根本不会)[self pp_setDelegate:delegate];}-(void)pp_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{//方案(通知)都用的到的通知,很显然太费事,不方便/*NSNotification *not = [[NSNotification alloc]initWithName:PPTableViewDidSelectedNotificationKey object:nil userInfo:nil];[[NSNotificationCenter defaultCenter]postNotification:not];*///方案(runtime):给tableView添加点击block,。。。。嗯,有没有想到更多??简化所有的delegate与dataSource方法,此处不说太多if (tableView.pp_didSelectedBlock) {tableView.pp_didSelectedBlock(tableView, indexPath);}//VIP: 此时,系统原生的didSelected方法已经被拦截,并且做了你想做的事情,可是怎么让系统原生的didSelected还能响应点击?/*方案一: 不用class_addMethod方法,而是在[delegate respondsToSelector:systemDidSelectedSelector]条件语句里添加代理绑定:objc_setAssociatedObject(PPTableViewDidSelectedNotificationKey, @selector(pp_tableView:didSelectRowAtIndexPath:), delegate, OBJC_ASSOCIATION_RETAIN);然后在此处,执行:id ppDelegate = objc_getAssociatedObject(PPTableViewDidSelectedNotificationKey, _cmd);[ppDelegate pp_tableView:tableView didSelectRowAtIndexPath:indexPath];哦😯,傻了,这相当于直接调用系统的didSelected方法,/(ㄒoㄒ)/~~,错!错!错!*///方案二:利用NSInvocation底层发消息,如下:SEL fakeDidSelectedSelector = @selector(pp_fakeTableView:didSelectRowAtIndexPath:);NSMethodSignature *methodSignature = [[tableView.delegate class]instanceMethodSignatureForSelector:fakeDidSelectedSelector];if (methodSignature == nil) {//可以抛出异常也可以不操作。}NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];invocation.target = tableView.delegate;invocation.selector = fakeDidSelectedSelector;[invocation retainArguments];[invocation invoke];}
2017年最后一天了,一首歌共勉: