A Segmented Control You Can't Click on Mac — Catalyst Doesn't Deliver Taps Into List Section Headers
Flick's range picker had a sort control that worked fine on iPhone and was completely dead on Mac. It rendered correctly, compiled without warnings, and shared one code path across both platforms. The only difference was that clicks never reached it. The cause: it lived in a List Section header.
Flick’s range picker is a sheet pulled up from the cleanup progress capsule: one flat timeline with “All Photos” pinned at the top, then the months, each row carrying a bar proportional to its volume. At the top sits a segmented control for sort order — by savable space, by size, by date.
On iPhone it had always worked.
On Mac, clicking it did nothing at all.
This bug was missing exactly one thing: the click
It’s worth spelling out how little this looked like a bug:
- It compiled, with no warnings on either platform.
- It rendered correctly. The segmented control drew where it should, with the selected segment highlighted, visually identical to iPhone.
- There was only one code path. Mac wasn’t taking a different branch; the same
Viewrendered on both platforms. - No crash, no error, clean console.
Its only problem was that pressing it did nothing. No highlight, no state change, the list didn’t budge.
A control that looks right, builds, and ships but receives no input is the kind of defect that survives a long time, because every automated gate waves it through.
The cause: section headers aren’t interactive on Catalyst
Flick’s macOS build is Mac Catalyst (SUPPORTS_MACCATALYST = YES) — the iOS UIKit stack running on the Mac.
The problem was where the control lived. It was attached to the Section’s header::
Section {
// month rows…
} 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 renders a List’s section header as a non-interactive supplementary view. Clicks are never delivered to controls inside it. Whatever you put there is a picture.
On iOS, header hit-testing works normally and the control receives taps. So this code was correct on iPhone from day one — the defect exists only on Catalyst, and iPhone is the primary test platform, which is how it survived until someone ran the Mac build.
The fix: move it from the header to the section’s first row
The fix is small — take the control out of header: and put it in the section body as the first row:
Section {
// Sort control is a ROW, not the section header it used to
// be: Mac Catalyst never delivers clicks to interactive
// controls inside List section headers (UIKit renders them
// as non-interactive supplementary views), so the sort
// picker was a completely dead segmented control on Mac.
// A regular row gets normal hit-testing on every platform.
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()
}
// month rows…
}
Regular List rows get normal hit-testing on every platform. One code path, correct on both, with no #if targetEnvironment(macCatalyst) fork.
Two modifiers came off along the way. .textCase(nil) existed solely to fight the header’s default uppercasing and is meaningless outside a header; .padding(.vertical, 2) was tuning for header spacing. What went on is .labelsHidden() — the Picker’s first argument is an accessibility label that renders visibly in a row, and the three segments already say what they do.
The visual result is actually more standard
The change moves the control from “floating above the list” to “the first row inside the list.”
I expected that to be a design concession made for a bug fix. It isn’t. In an inset grouped list, “a control as the first row of a group” is the standard form — macOS System Settings is full of it. The original floating segmented header was more of an iOS habit carried over unexamined.
When a fix forces you into a different presentation, it’s worth checking whether the one you’re moving to is the platform’s native idiom. Here it was, so the fix cost nothing.
The pattern worth keeping
On Catalyst, never put an interactive control in a List section header. Buttons, toggles, segmented controls, menus — none of them. Move it to a row, or to the navigation bar or toolbar.
This deserves more shelf space than it looks like it needs, because of how gently it fails: no compile error, no runtime warning, no crash, and the rendering is perfect. No amount of iPhone testing surfaces it, and on the Mac you won’t see it in a screenshot — only by actually reaching over and clicking.
The cost of cross-platform reuse usually isn’t “it doesn’t run,” it’s “it runs, with half the behavior missing.” Differences like this don’t announce themselves; the only way to find them is to genuinely use the build on every target platform. Which is why the second thing I did after this fix was click through every other interactive control on the Mac — if there are more Catalyst-only dead controls, they almost certainly share this root cause.
Comments