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 小时前
一个简单的套壳方案,就能让你的 Agent 少做重复初始化
前端
问心无愧05131 小时前
ctf show web入门27
前端
小村儿1 小时前
给 AI Agent 装上"长期记忆":Karpathy 的 LLM Wiki 思想,我做成了工具
前端·后端·ai编程
竹林8182 小时前
用ethers.js连接MetaMask实现Web3钱包登录:从踩坑到稳定运行的完整记录
前端·javascript
heyCHEEMS2 小时前
如何用 Recast 实现静态配置文件源码级读写
前端·node.js
心连欣2 小时前
从零开始,学习所有指令!
前端·javascript·vue.js
review445432 小时前
大模型和function calling分别是如何工作的
前端
东东同学2 小时前
耗时一个月,我把 Nuxt 首屏性能排障经验做成了一个 AI Skill
前端·agent
冴羽3 小时前
超越 Vibe Coding —— AI 辅助编程指南
前端·ai编程·vibecoding
梦想的颜色3 小时前
一天一个SKILL——前端最佳自动化测试 webapp-testing
前端·web app