• 首页
  • 博客
  • 项目
  • 留言墙
  • 问题现象
  • 顺着"背景闪一下"往下挖
  • 第二个坑:popover 不会自己拿 key window
  • 修复
  • 一点经验
macOS 15 上 SwiftUI popover 里中文输入法选不了词——一次双 bug 排查
2026/04/27macOS

macOS 15 上 SwiftUI popover 里中文输入法选不了词——一次双 bug 排查

Zipic 用户在 macOS 15 上报了个怪 bug:popover 里改预设名,中文输入法按空格选不了词,我复现了,同时发现那一行的背景还会闪一下。但在 macOS 26 上却完全正常。顺着"背景闪一下"这条线索挖下去,竟挖出两个叠加的 bug——而 Apple 在 macOS 15 上的一处修复,恰好就把这个问题翻出来了。

430次点击6分钟阅读

最近收到 Zipic 用户反馈:在 macOS 15.5 上,给预设方案改名时,中文输入法选不了词——拼音敲进去能看到候选,但按空格那一下,候选词就是落不到输入框里。

复现了一遍,确实如此。更怪的是,按空格的同时,那一行 preset 的背景颜色会闪一下,像是被重新选中了一次。

这条"背景闪一下"的线索后来成了破案的钥匙。整个查错过程踩到的两个坑都很典型,单独看都不算冷门,叠加在一起却拼出了一个看似无解的怪 bug。

问题现象

复现 GIF:popover 中拼音空格选词无法 commit

复现路径很简单:偏好设置 → 预设方案 → 点某行的"编辑名称"按钮弹出 popover → 切到中文输入法 → 在 TextField 里输拼音 → 按空格选词。

预期是把候选词提交到输入框,实际是候选词消失、输入框空空,行背景闪一下。

一开始我顺着 @FocusState、popover 层级、IME marked-text 几个方向都怀疑过,换了几个写法没解决。后来在 macOS 26 上跑了一遍——完全正常。

这下问题反过来了:不是"我写错了什么",而是"15 和 26 之间,系统行为变了什么"。

顺着"背景闪一下"往下挖

预设行原本的代码大致是这样:

1
2
3
4
Button(action: { presetViewModel.select(preset) }) {
    HStack { ... }   // 整行内容
}
.buttonStyle(.plain)

整行被一个外层 Button 包着,点行内空白处就能选中预设——挺常规的写法。

那"闪一下"意味着什么?意味着这个 Button 的 action 被触发了。换句话说:用户按下的那个空格,没去 TextField,而是把外层那个 Button 给点了一下。

去查 SwiftUI Button 在 macOS 15 上的行为变更,立刻找到了 Christian Tietze 的一篇文章 https://christiantietze.de/posts/2024/07/swiftui-button-click-through-fixed-macos-15/:从 macOS 15 Beta 2 起,SwiftUI 的 Button(包括 .plain 风格)跟原生 NSButton 行为对齐了——focus 在它上面时,按空格会激活它。

对绝大多数场景这是好事,但对"整行包成 Button"的写法就是灾难:popover 弹出后空格落回外层 Button 头上,preset 被"重新 select"了一次,所以背景闪一下。这是 Bug 一。

但只有 Bug 一还不够:popover 都弹出来了,里面 TextField 也写了 @FocusState,焦点为什么没接管过去?

第二个坑:popover 不会自己拿 key window

SwiftUI 的 .popover() 在 macOS 上是用 NSPopover 实现的,渲染在独立的 NSPopoverPanel 里。社区里有个流传很久的坑:这个 panel 不会可靠地自动 makeKey(),主窗口仍然是 key window。

后果是:IME 候选词浮窗是 OS 级别的、跟 key window 无关,所以"看上去候选词正常显示";但 IME 关联的 input context 仍在主窗口某个 control 上,不在 popover 里的 NSTextField 上;@FocusState 也不跨 NSWindow,panel 不是 key window 时它压根没法把 first responder 真正交给 popover 里的 TextField。于是用户按下的空格事件,被路由到了主窗口去——

空格事件被路由到主窗口的示意

正好撞上主窗口的 first responder——那行包了外层 Button 的 preset。Bug 一与 Bug 二合流,闪烁与无法 commit 同时出现。

macOS 26 不复现,是因为 26 重写了 SwiftUI 的容器/窗口层(Liquid Glass 配套),popover panel 的 makeKey 它内部接管了,Bug 二被消除,Bug 一单独不再触发可观察症状。

修复

定位到根因,修复就直接了:

第一,拆掉外层 Button。 整行点击改用 HStack + .contentShape(Rectangle()) + .onTapGesture,行为等价但不会被空格激活,顺手消除一处历史遗留的 .onTapGesture { /* prevent click-through */ }。

第二,强制 popover panel 成为 key window。 在 popover 内容里挂一个一次性 NSViewRepresentable,onAppear 时调一句 view.window?.makeKey()——这就是 macOS 26 替我们做的事,15.x 上手工补上。

第三,删掉 popover 周边按钮上的 .keyboardShortcut(.return) / .keyboardShortcut(.escape),让 TextField 自己的 .onSubmit 处理回车。keyboardShortcut 会让按钮成为 default/cancel button 参与 popover 内的按键分发,对 IME marked-text 阶段会有干扰。

第四,清掉项目里历史遗留的 KeyPressHandlingView——自己继承 NSView、实现 acceptsFirstResponder 和 keyDown 但不调 super.keyDown / interpretKeyEvents 的拦截器,是 IME 杀手,碰上中日韩输入法的 marked-text 流就会乱。

一点经验

macOS 15 vs 26 同场景行为对比

写 macOS SwiftUI 的人多多少少都会遇到 popover + TextField + 焦点的奇怪问题。这次让我意识到的是:

当一个 bug 只在某个系统版本上出现、又在新版本上消失时,不要急着把它归为"系统 bug,等 Apple 修"。 真正发生的事,往往是新系统的某个修复把你代码里早就存在、但一直被掩盖的另一个 bug 翻了出来。

这个 case 里,"行背景闪一下"是用户多说的一句话,正是这句话把我从"IME 出问题了"的方向,拽回到"按键事件被路由到了不该去的地方"。如果用户只说"空格选词不行",我大概率会在 IME / @FocusState 一带绕很久。

诊断 bug 时,最不像问题的那个细节,往往是最关键的线索。

☕️支持一下

如果我的内容对你有帮助,可以请我喝杯咖啡 🫶

相关文章

macOS 自启动服务配置指南

2026/02/10macOS4570分钟阅读

© 2026 • 5km • Fork 自cali.so

首页博客项目留言墙
总浏览量 —
支持一下