Mac 上点不动的分段控件——Catalyst 不会把点击投递进 List 的 section header
Flick 的范围选择器有个排序控件,在 iPhone 上一直好好的,装到 Mac 上却完全点不动。它渲染正常、编译无警告、代码两端共用一份,唯一的差别是点击到不了它。根因是它被放在了 List 的 Section header 里。
Flick 的范围选择器是一张从清理进度胶囊拉起来的表:一条平铺的时间线,「全部照片」钉在顶部,下面是各个月份,每行带一条与体积成正比的横条。顶上有个分段控件切换排序——按可省、按体积、按时间。
在 iPhone 上,它一直工作得好好的。
装到 Mac 上,点它没有任何反应。
这个 bug 什么都不缺,只缺点击
先说清楚它有多不像一个 bug:
- 编译通过,两个平台都没有警告。
- 渲染正常。分段控件画在该在的位置,当前选中项高亮,视觉上和 iPhone 上一模一样。
- 代码只有一份。不是 Mac 上走了另一条分支,是同一个
View在两个平台上渲染。 - 不崩溃、不报错、控制台干净。
它唯一的问题是:点下去,什么都没发生。没有高亮反馈,没有状态变化,列表纹丝不动。
一个控件长得对、装得上、编得过,只是收不到点击——这类缺陷最容易活很久,因为所有自动化的关卡都放它过。
根因:section header 在 Catalyst 上不是可交互的
Flick 的 macOS 版本是 Mac Catalyst(SUPPORTS_MACCATALYST = YES),也就是把 iOS 的 UIKit 那套跑在 Mac 上。
问题出在这个控件被放在哪里。原来的写法是把它挂在 Section 的 header: 里:
Section {
// 月份行…
} header: {
if buckets.count > 1 {
Picker("排序", selection: $sortRaw) {
Text("按可省").tag(SortMode.savable.rawValue)
Text("按体积").tag(SortMode.bytes.rawValue)
Text("按时间").tag(SortMode.time.rawValue)
}
.pickerStyle(.segmented)
.textCase(nil)
.padding(.vertical, 2)
}
}
**Mac Catalyst 把 List 的 section header 渲染成不可交互的 supplementary view。**点击永远不会被投递到 header 内部的控件里。不管你在里面放什么,它都只是一张画。
iOS 那边 header 的命中测试是正常的,控件收得到点击。所以这段代码在 iPhone 上从头到尾都对——这个缺陷只在 Catalyst 上存在,而 iPhone 是主力测试平台,于是它一路活到了装进 Mac 才露头。
修法:从 header 挪成 section 的第一行
修复很简单——把控件从 header: 拿出来,放进 section 内容,当成第一行:
Section {
// 排序控件现在是一「行」,不再是 section header:
// Mac Catalyst 从不把点击投递给 List section header 里的交互控件
// (UIKit 把 header 渲染成不可交互的 supplementary view),
// 所以按可省/按体积/按时间在 Mac 上是个彻底失灵的分段控件。
// 普通行在所有平台上都有正常的命中测试。
if buckets.count > 1 {
Picker("排序", selection: $sortRaw) {
Text("按可省").tag(SortMode.savable.rawValue)
Text("按体积").tag(SortMode.bytes.rawValue)
Text("按时间").tag(SortMode.time.rawValue)
}
.pickerStyle(.segmented)
.labelsHidden()
}
// 月份行…
}
**普通的 List 行在所有平台上都有正常的命中测试。**一份代码,两端都对,不需要 #if targetEnvironment(macCatalyst) 分叉。
顺手清掉了两个修饰符:.textCase(nil) 是专门用来对抗 header 默认大写的,搬出 header 之后没有意义了;.padding(.vertical, 2) 也是为了贴合 header 的间距。补上的是 .labelsHidden()——Picker 的第一个参数 "排序" 是给无障碍用的标签,在行里会被渲染出来,而分段控件的三个选项已经自解释了。
视觉上反而更标准了
这个改动把控件从「悬浮在列表上方」变成了「列表内的第一行」。
一开始我担心这是为了修 bug 而牺牲设计,实际不是。在 inset grouped 列表里,「控件作为分组内第一行」本来就是标准形态——macOS 的系统设置里到处是这个样式。反倒是原来那种浮在 header 上的分段控件,更像 iOS 的习惯被直接搬过来。
修 bug 的时候遇到「不得不换一种呈现」,值得先查一下换过去的那种是不是平台的原生惯例。这次是,所以没有代价。
留下的模式
**Catalyst 上不要往 List 的 section header 里放任何交互控件。**按钮、开关、分段控件、菜单,一律不行。要么挪成行,要么挪到导航栏或工具栏。
这条比它看上去更值得记,因为它的失败方式特别温和:没有编译错误,没有运行时警告,没有崩溃,视觉完全正常。你在 iPhone 上怎么测都测不出来,而在 Mac 上如果没有真的伸手点一下,光看截图也发现不了。
**跨平台复用的代价往往不在“跑不起来”,而在“跑起来了但少了一半行为”。**这类差异不会主动报告自己,只能靠在每个目标平台上真的动手用一遍。所以修完这个之后,我做的第二件事是把 Mac 上其他的交互控件挨个点了一遍——如果还有别的 Catalyst 独有的失灵控件,大概率是同一个根因。
留言