使用Runtime解决 cell 点击时子视图改变背景颜色的问题

前言

iOS 开发中,UITableView 随处可见,而在点击 UITableView 的 cell 的时候,如果他的子视图设置了透明颜色以外的颜色,子视图的背景颜色会进行相关的改变,效果如下图。

cell 被点击时,子视图的背景颜色会产生变化.gif

这种情况是不是有种似曾相识的感觉

如果没有,我再举几个很多人使用的 App 上对于这种情况处理不佳的例子,注意左右对比

新浪微博 cell 点击时消失的灰色分割线

简书 cell 点击时消失的蓝色小圈圈

产生这种情况的原因是因为 cell 在点击的时候会将子视图的背景颜色设置为透明色,而这里微博的分割线和简书的蓝色小圈圈应该是用了一种 UIView 设置了背景颜色来实现的,于是在点击过程中,本来是灰色的分割线,和本来是蓝色的小圈圈,都“消失”了。

网上对于这种情况的解决方案,大多是下面这种解决方式:

修改 cell 的选中样式:

 
  1.  
  2. cell.selectionStyle = UITableViewCellSelectionStyleNone;

这种解决方案,可以正确的达到点击的时候子视图背景颜色不再改变,只是美中不足的是,这种方法不仅去除了我们不想要的子视图背景颜色改变的效果,还去除了 cell 本身 contentView 的背景颜色改变的效果。

此时点击 cell ,用户不再会感觉到有任何变化,为了取消子视图背景颜色改变效果,而取消 cell 的选中效果,这种做法不太友好。

此时又有网友提出,自己实现这种选中效果,在 cell 的 contentView 的最下层添加一个 button,很好的思路,只是,如果此时你的 cell 已经使用 xib 布局的差不多了,或者使用纯代码写的差不多了,再向 cell 的 contentView 和你所添加的 subview 中间添加一个 button 就显得有点麻烦。

懒癌晚期,想有一劳永逸的解决办法,最好是那种不对原工程做任何改动的

下面是我思考的过程:

我先看看 cell 的 contentView 中所有子视图调用设置背景颜色的方法时的函数调用栈,看看颜色改变成透明眼色之前调用了哪些方法

cell 子视图更改颜色之前,究竟做了些什么?

我新建了一个 .m 文件,并在其中使用 runtime 的 Method Swizzle 对设置背景颜色方法进行替换,在新的设置背景颜色方法中打印函数调用栈,具体代码如下:

使用 runtime 打印设置背景颜色透明时的函数调用栈

这里如果不懂运行时的相关用法,可以自行去了解一下 runtime 的相关使用以及概念。

然后我们运行进行相关测试,发现打印了两次函数调用栈,意味着,在点击开始和结束之后,cell 的子视图有两次被设置成了透明颜色。其中一次是点击开始时,改变子视图背景颜色为透明色,还有一次是点击结束时,改变子视图背景颜色为透明色。

点击 cell 改变子视图背景颜色时的函数调用栈

取消点击 cell 时恢复子视图背景颜色时的函数调用栈

对比点击前后的函数调用栈,我们可以观察到,在点击 cell 的设置子视图背景颜色的前后都调用了

 
  1.  
  2. _setOpaque:forSubview:
  3. showSelectedBackgroundView:animated:
  4. setHighlighted:animated:

等方法;
其中:

  • 前两个方法是私有 API,使用私有 API 的结果无法预料。。可能会被苹果爸爸拒绝上线,所以,这里不考虑他们
  • 最后一个 setHighlighted:animated: 方法在点击时,传入的 highlighted 为 YES,点击结束传入的为 NO。似乎可以考虑在这里里一个 FLAG
  • 另外,在 cell 默认加载出来的时候也会调用 setHighlighted:animated: 方法,只不过传入的 highlighted 为 NO
  • 那么此处我已经确定可以用最后一个方法立一个 FLAG 了,也就是我在这里,只需要在 highlighted 传入为 YES 的时候让子视图禁止调用改变背景颜色的方法,然后在再次为 NO 的时候,恢复可以调用改变背景颜色。
  • 这似乎是一个可行的方法,但是,不要忘了,函数调用栈中的方法,是越早调用的方法越在下面,这里 setHighlighted:animated: 方法的调用在最后一次设置背景颜色为透明之前,也就是说,如果我按照上面的方法做了,那我就只能阻止一次设置背景颜色为透明
  • 我想到的解决方案,再立一个 FLAG,然后,在 highlighted 为 YES 的时候,让子视图禁止设置背景颜色,然后第一次设置为透明会被阻止,第二次的时候,判断 FLAG,通过 FLAG 再阻止下一次设置背景颜色为透明

禁止设置子视图背景颜色的具体代码如下:

禁止 UILabel 设置背景颜色

这段代码使用了 runtime 在交换的设置背景颜色方法中,对通过 runtime 添加的 forbidSetBackgroundColor 属性进行判断,如果设置为 YES,则不能设置背景颜色。

接下来对 cell 的 setHighlighted:animated: 进行替换,在合适的时机,设置 UILabel 能否改变背景颜色即可

具体代码如下:

替换 UITableViewCell 的 setHighlighted-animated- 方法

接下来,我们该立 FLAG 了

通过 Flag 阻止最后一次设置背景颜色为透明的操作

我们添加了一个新的属性,当 highlighted 为 YES 的时候,forbidSetBackgroundColor 会变成 YES,此时,FLAG:shouldIgnoreSetClearBackgroundColorHandle 设置为 YES,下一次 forbidSetBackgroundColor 不起作用的时候,通过 shouldIgnoreSetClearBackgroundColorHandle 可以再次阻止背景色被设置成透明色的操作。

写完这些代码,我想我以后又可以偷懒了,不需要以后每次去关心 cell 点击改变子视图背景颜色的问题,只需要新增一个 .m 文件,不需要改动原先一句代码,就可以实现点击 cell 前后,子视图颜色不会改变,而且也保留了原生的点击效果

不足之处

这篇文章中所实现的仅仅是 UITableViewCell 在点击过程中 UILabel 的颜色不会改变,其他的子控件,诸如,UIButton,UIView 之类的视图,背景色仍然会改变,而且也不能解决多个视图层叠的背景色被清空的情况,于是,我写了一个分类:CoderGin/CGCellContentViewManager

  • 实现了所有带有非透明颜色的 UI 控件在 UITableViewCell 中,点击过程中不会改变背景颜色的效果
  • 使用方法很简单,直接将分类拖入工程中,即可使用
  • 欢迎大家使用和 Star,欢迎提出修改意见,更欢迎提出 bug

感谢

在这里要感谢两个网友 angelen10 和 MuYanQin 对我所写的代码的 bug 提出,欢迎更多人指出我的 bug。

结尾

  • 最后附上 .m 文件所有的代码
  • 使用方法很简单,在工程中新增一个 .m 文件,把下方的所有代码粘贴上去
  • 不需要你调用和改变任何原先的代码,这段代码就会自动工作了,
  • 如果你有什么疑问,或者发现了我代码中有什么不对的地方,欢迎评论和纠错
  • 如果你想研究和阅读本篇文章测试所写的工程,可以到我的 GitHub: CoderGin/UITableViewDemo上下载
 
  1.  
  2. #import <UIKit/UIKit.h>
  3. #import <objc/runtime.h>
  4.  
  5. @interface UILabel (RuntimeTest)
  6.  
  7. @property (nonatomic, assign) BOOL forbidSetBackgroundColor;
  8.  
  9. @property (nonatomic, assign) BOOL shouldIgnoreSetClearBackgroundColorHandle;
  10.  
  11. @end
  12.  
  13. @implementation UILabel (RuntimeTest)
  14.  
  15. + (void)load {
  16.  
  17. SEL originalSelector = @selector(setBackgroundColor:);
  18. SEL swizzledSelector = @selector(ex_setBackgroundColor:);
  19.  
  20. Method originalMethod = class_getInstanceMethod(self, originalSelector);
  21. Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
  22.  
  23. BOOL success = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
  24. if (success) {
  25. class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  26. } else {
  27. method_exchangeImplementations(originalMethod, swizzledMethod);
  28. }
  29. }
  30.  
  31. - (void)ex_setBackgroundColor:(UIColor *)backgroundColor {
  32.  
  33. if (self.forbidSetBackgroundColor){
  34. self.shouldIgnoreSetClearBackgroundColorHandle = YES;
  35. } else {
  36. if (backgroundColor == [UIColor clearColor]) {
  37. if (self.shouldIgnoreSetClearBackgroundColorHandle) {
  38. self.shouldIgnoreSetClearBackgroundColorHandle = NO;
  39. } else {
  40. [self ex_setBackgroundColor:backgroundColor];
  41. }
  42. } else {
  43. [self ex_setBackgroundColor:backgroundColor];
  44. }
  45. }
  46. }
  47.  
  48. - (BOOL)shouldIgnoreSetClearBackgroundColorHandle {
  49.  
  50. id value = objc_getAssociatedObject(self, _cmd);
  51. if (value == nil) {
  52. objc_setAssociatedObject(self, _cmd, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  53. }
  54. return [objc_getAssociatedObject(self, _cmd) boolValue];
  55. }
  56.  
  57. - (void)setShouldIgnoreSetClearBackgroundColorHandle:(BOOL)shouldIgnoreSetClearBackgroundColorHandle {
  58.  
  59. objc_setAssociatedObject(self, @selector(shouldIgnoreSetClearBackgroundColorHandle), @(shouldIgnoreSetClearBackgroundColorHandle), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  60. }
  61.  
  62. - (BOOL)forbidSetBackgroundColor {
  63.  
  64. id value = objc_getAssociatedObject(self, _cmd);
  65. if (value == nil) {
  66. objc_setAssociatedObject(self, _cmd, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  67. }
  68. return [objc_getAssociatedObject(self, _cmd) boolValue];
  69. }
  70.  
  71. - (void)setForbidSetBackgroundColor:(BOOL)forbidSetBackgroundColor {
  72.  
  73. objc_setAssociatedObject(self, @selector(forbidSetBackgroundColor), @(forbidSetBackgroundColor), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  74. }
  75.  
  76. @end
  77.  
  78. @implementation UITableViewCell (RuntimeTest)
  79.  
  80. + (void)load {
  81.  
  82. SEL originalSelector = @selector(setHighlighted:animated:);
  83. SEL swizzledSelector = @selector(ex_setHighlighted:animated:);
  84.  
  85. Method originalMethod = class_getInstanceMethod(self, originalSelector);
  86. Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
  87.  
  88. BOOL success = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
  89. if (success) {
  90. class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  91. } else {
  92. method_exchangeImplementations(originalMethod, swizzledMethod);
  93. }
  94. }
  95.  
  96. - (void)ex_setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
  97.  
  98. if (highlighted == YES) {
  99. for (UIView *subview in self.contentView.subviews) {
  100. if ([subview isKindOfClass:[UILabel class]]) {
  101. UILabel *label = (UILabel *)subview;
  102. label.forbidSetBackgroundColor = YES;
  103. }
  104. }
  105. } else {
  106. for (UIView *subview in self.contentView.subviews) {
  107. if ([subview isKindOfClass:[UILabel class]]) {
  108. UILabel *label = (UILabel *)subview;
  109. label.forbidSetBackgroundColor = NO;
  110. }
  111. }
  112. }
  113. [self ex_setHighlighted:highlighted animated:animated];
  114. }
  115.  
  116.  
  117. @end

 

 

原文:http://bbs.520it.com/forum.php?mod=viewthread&tid=2781

转载于:https://my.oschina.net/u/2345393/blog/791468

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值