iOS | UIPickerView 如何优雅修改选中样式?

本文会介绍两个我在使用 UIPickerView 时遇到的问题, 终究给出它们的解决方法:

  1. 怎么无推迟地更新选中行的款式?
  2. 怎么修正选中行的布景色彩?

选中行的款式

需求中,要求更改选中行的字体色彩和字重,加粗+变色。我一开始尝试在「选中回调函数pickerView:didSelectRow:inComponent: 」 这个回调函数中,获取当时选中的 view,然后修正它的款式,可是这样的作用不可丝滑,需求松开手、有大约 0.5s 的推迟,才会看到选中作用

iOS | UIPickerView 如何优雅修改选中样式?

同时还有一个问题,便是在用户还没有开始翻滚挑选之初,当时选中行是没有「选中作用」的。哪怕手动调用selectRow:inComponent:animated: 函数,pickerView 尽管也滑到了指定行,却不会触发pickerView:didSelectRow:inComponent: 这个回调。

这种不完全的作用显然不能被规划搭档接受。

这个问题其实我还没研讨清楚原因,等待有知道的大佬指点一下迷津

所以我去github 上看看有没有人解决了这个问题,找到了一个 star 数有2k+的BRPickerView,人家的选中作用就很丝滑,翻了翻代码,发现ta 是在pickerView:viewForRow:forComponent:reusingView: 中设置当时选中行的款式,如下:

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
  // 设置选中行字体色彩和字体大小
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    UILabel *label = (UILabel *)[pickerView viewForRow:row forComponent:component];
    label.textColor = UIColor.blueColor;
     [label setFont:[UIFont systemFontOfSize:20 weight:20]];
   });
  }
}

可是为什么要放在dispatch_after 里当即履行,这个暂时也不太清楚,我自己动手测验,不包这一层不影响作用,猜测是包了这一层后能够确保其间的代码能够立刻在主行列履行,同时避免发生子线程修正 UI 而 crash 的情况。

看了下pickerView:viewForRow:forComponent:reusingView: 的文档,

Called by the picker view when it needs the view to use for a given row in a given component. *当 pickerView 需求那个 view 用于指定列中的指定行时,它会调用该方法来获取。 *

我自己通过测验,初始化和翻滚时都会调用这个回调,并且不止一次

初始化:-[UIPickerView tableView:cellForRowAtIndexPath:] ()。5 个元素的 pickerView 调用了 **8 次该函数 滑动:-[UIPickerView tableView:cellForRowAtIndexPath:] (),调用 2 次


anyway,把修正款式的代码放到 pickerView:viewForRow:forComponent:reusingView: 后,不仅丝滑了很多,并且在初始、用户未翻滚的时分也能确保有选中作用,需求成功完成。 如下:

iOS | UIPickerView 如何优雅修改选中样式?

修正选中行的布景色彩

还有一个常见的需求便是修正选中行的布景色彩,如上图,选中行的 View 我们能够通过 [pickerView viewForRow:row forComponent:component] 这样的代码获取,可是有问题, 如果给这个 View 设置布景色彩,前面会留有空地,如图:

iOS | UIPickerView 如何优雅修改选中样式?

这办法还不可,得找到外部的容器,修正它的布景色彩才行。在 BRPickerView 中找到的方法如下:

// 设置选中行布景色
  // 1. 获取 pickerView 的子 View(有多个),取第一个 view,称之为 contentView
  UIView *contentView = nil;
  NSArray *subviews = pickerView.subviews;
  if (subviews.count > 0) {
    id firstObj = subviews.firstObject;
    if (firstObj && [firstObj isKindOfClass:[UIView class]]) {
      contentView = (UIView *)firstObj;
     }
   }
  
  // 2. 在 contentView 中通过 KVC 取它的 subviewCache 特点,称为 columnView
  UIView *columnView = nil;
  if (contentView) {
    id obj = [contentView valueForKey:@"subviewCache"];
    if (obj && [obj isKindOfClass:[NSArray class]]) {
      NSArray *columnViewArray = (NSArray *)obj;
      if (columnViewArray.count > 0) {
        id columnObj = columnViewArray.firstObject;
        if (columnObj && [columnObj isKindOfClass:[UIView class]]) {
          columnView = (UIView *)columnObj;
         }
       }
     }
   }
  
  // 2.还用 KVC,取 columnView 的 middleContainerView,修正它的布景色彩即可
  if (columnView) {
    id containerObj = [columnView valueForKey:@"middleContainerView"];
    if (containerObj && [containerObj isKindOfClass:[UIView class]]) {
      UIView *selectedRowViewContainer = (UIView *)containerObj;
      // 终究设置布景框的色彩
      selectedRowViewContainer.backgroundColor = UIColor.blackColor;
     }
   }

到这里我不禁想为什么 Apple 的规划思路这么乖僻,这么一个常用的特点为什么不露出给开发者呢,就叫 selectedRowViewContainer 之类的。

忽然! 我又想到了一个别的方法,已然里层选中行的 View 能够获取到,而容器必定是包括这个「选中行 View」,那我从「选中行 View」出发,一路找它的父视图,必定能够找到哇!这种方法的代码量就少很多了。

通过测验,终究的代码是:

UILabel *label = (UILabel *)[pickerView viewForRow:row forComponent:component];
UIView *container = [[[[label superview] superview] superview] superview]; // 4个 superView

iOS | UIPickerView 如何优雅修改选中样式?

作用是一样的

iOS | UIPickerView 如何优雅修改选中样式?

至此,文章最初说的两个问题圆满解决!庆祝


说点题外话,这个需求其实是临时加上来的,排期很紧张,我对UIPickerView 这个控件完全没有经验,就想着找一找项目里曾经有没有用过,然后把它抄过来。可是一向没有解决好文章说到的第一个问题,加上隔壁 Android 的搭档很快就写出来了,越写越急。其实是应该沉下心来的,认真读文档和三方库,而不是没有方案、没有规矩地乱试,这个是我自己的缺点,我要改正。


PS. 在研讨这个问题的时分发现,拼多多App 中的「我的材料」页,其间的地区挑选包括的 pickerView 也有这个推迟情况。