使用Runtime解決 cell 點擊時子視圖改變背景顏色的問題

前言

iOS 開發中,UITableView 隨處可見,而在點擊 UITableView 的 cell 的時候,若是他的子視圖設置了透明顏色之外的顏色,子視圖的背景顏色會進行相關的改變,效果以下圖。php

cell 被點擊時,子視圖的背景顏色會產生變化.gif

這種狀況是否是有種似曾相識的感受

若是沒有,我再舉幾個不少人使用的 App 上對於這種狀況處理不佳的例子,注意左右對比git

新浪微博 cell 點擊時消失的灰色分割線

簡書 cell 點擊時消失的藍色小圈圈

產生這種狀況的緣由是由於 cell 在點擊的時候會將子視圖的背景顏色設置爲透明色,而這裏微博的分割線和簡書的藍色小圈圈應該是用了一種 UIView 設置了背景顏色來實現的,因而在點擊過程當中,原本是灰色的分割線,和原本是藍色的小圈圈,都「消失」了。github

網上對於這種狀況的解決方案,大可能是下面這種解決方式:函數

修改 cell 的選中樣式:佈局

 
  1.  
  2. cell.selectionStyle = UITableViewCellSelectionStyleNone;

這種解決方案,能夠正確的達到點擊的時候子視圖背景顏色再也不改變,只是美中不足的是,這種方法不只去除了咱們不想要的子視圖背景顏色改變的效果,還去除了 cell 自己 contentView 的背景顏色改變的效果。測試

此時點擊 cell ,用戶再也不會感受到有任何變化,爲了取消子視圖背景顏色改變效果,而取消 cell 的選中效果,這種作法不太友好。atom

此時又有網友提出,本身實現這種選中效果,在 cell 的 contentView 的最下層添加一個 button,很好的思路,只是,若是此時你的 cell 已經使用 xib 佈局的差很少了,或者使用純代碼寫的差很少了,再向 cell 的 contentView 和你所添加的 subview 中間添加一個 button 就顯得有點麻煩。spa

懶癌晚期,想有一勞永逸的解決辦法,最好是那種不對原工程作任何改動的

下面是我思考的過程:.net

我先看看 cell 的 contentView 中全部子視圖調用設置背景顏色的方法時的函數調用棧,看看顏色改變成透明眼色以前調用了哪些方法code

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

相關文章
相關標籤/搜索