某天,闲来无聊点击微信通讯录右边的索引(具体操作如下:打开微信—通讯录,然后右手点击某个索引,手指别松开,然后左手点击某个联系人cell
,跳转后再返回,你刚才长按的放大索引还在,并且再滑动,索引对应字母也不变化,索引视图卡住。),发现了这个bug
(我都滑动到L了,放大的索引还是M
),如下:
由于自己的项目也有类似的功能,同样操作,也有同样的bug
,于是就赶紧修复一下。
问题有了,解决问题的关键就是找到原因并给出合理的解决方案。
Why?
Why?
Why?
原因:放大的索引视图在显示的情况下点击tableViewCell
没有隐藏掉.
解决方案:在didSelectRowAtIndexPath
方法里隐藏(or
移除)放大了的索引视图。
- 方案一:用通知,
didSelectRowAtIndexPath
调用的时候给**PPSectionTitleIndexView
**发个通知。
- 方案二 :用
runtime
,didSelectRowAtIndexPath
调用的时候给UItableView
添加一个block
,**PPSectionTitleIndexView
**里面的tableView
实现block
。
两种方案对比:
方案一: 需要每次调用didSelectRowAtIndexPath
时都要发送一个通知;
方案二: 什么也不用做。所以,当然方案二更好。
扯了那么多,现在进入实战。
第一步 :创建PPSectionTitleIndexView
继承自UIView
,并声明代理PPSectionTitleIndexViewDelegate
,代码如下:
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
|
#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;
@end
NS_ASSUME_NONNULL_END
|
第二步:设置PPSectionTitleIndexView
思路:每个索引用UIButton
,当前点击的放大索引用UILabel
显示。
核心代码如下:
1.创建UI
的
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
|
-(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.响应事件的
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
|
-(void)btnClickedDown:(UIButton *)sender
{
//1. 获取当前sectionTitle的frame
CGRect 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. 处理indexStr
if (currentIndexStr.length == 1) {
self.currentIndexShowLB.font = [UIFont systemFontOfSize:35];
}else{
self.currentIndexShowLB.font = [UIFont systemFontOfSize:18];
}
//5. 屏幕中间显示当前index
self.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. 获取手指当前位置的point
CGPoint 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. 手势结束时,隐藏屏幕中间的showIndexLB
if (panGR.state == UIGestureRecognizerStateEnded) {
self.currentIndexShowLB.hidden = YES;
}
}
|
至此,想要的功能算是实现了,但是文章一开始提到的bug也出现了,😜。
第三步:解决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:
,这样就实现了我们的需求。
我怕我说明的不清楚,特意画了个图:
代码如下:
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
|
///**
// * 通过给定的方法名和实现,动态给类添加一个方法
// *
// * @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年最后一天了,一首歌共勉: