本文会介绍两个我在使用 UIPickerView 时遇到的问题, 最后给出它们的解决方法:
- 如何无延迟地更新选中行的样式?
- 如何修改选中行的背景颜色?
选中行的样式
需求中,要求更改选中行的字体颜色和字重,加粗+变色。我一开始尝试在「选中回调函数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 也有这个延迟情况。