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

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

  1. 如何无延迟地更新选中行的样式?
  2. 如何修改选中行的背景颜色?

选中行的样式

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

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

这种不彻底的效果显然不能被设计同事接受。

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

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

objc 复制代码
- (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: 后,不仅丝滑了很多,而且在初始、用户未滚动的时候也能保证有选中效果,需求成功实现。 如下:

修改选中行的背景颜色

还有一个常见的需求就是修改选中行的背景颜色,如上图,选中行的 View 我们可以通过 [pickerView viewForRow:row forComponent:component] 这样的代码获取,但是有问题, 如果给这个 View 设置背景颜色,前面会留有空隙,如图:

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

objc 复制代码
// 设置选中行背景色
    // 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」出发,一路找它的父视图,肯定可以找到哇!这种方法的代码量就少很多了。

经过测试,最终的代码是:

objc 复制代码
UILabel *label = (UILabel *)[pickerView viewForRow:row forComponent:component];
UIView *container = [[[[label superview] superview] superview] superview]; // 4个 superView
​

效果是一样的

至此,文章开头说的两个问题圆满解决!🎉庆祝


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


PS. 在研究这个问题的时候发现,拼多多App 中的「我的资料」页,其中的地区选择包含的 pickerView 也有这个延迟情况。

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐4 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄5 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser7 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui7 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui