升级改造你的 AppKit App
Modernize your AppKit app
2026年6月10日
一句话判断
✅ AppKit 彻底抛弃 mouseDown: 转向手势识别器(Gesture Recognizers),并用全新的“边角同心性(Concentricity)”API 终结了 macOS 嵌套圆角对不齐的强迫症。
这场 Session 讲了什么
这场 Session 最值得关注的一件事是:AppKit 终于把 UIKit 早就用熟的输入模型和 UI 自动化能力全面补齐了。AppKit 作为 macOS 的元老级框架,历史包袱太重,很多老代码还在用 mouseDown: 和追踪循环(Tracking loops)硬写鼠标交互。这不仅和 SwiftUI、UIKit 的输入模型割裂,还经常导致事件吞没和状态混乱。这次 Apple 下了狠手,把手势识别器和控制事件(Control Events)作为处理输入的“唯一正解”强推给开发者。
除了输入模型,macOS 的 UI 细节也迎来了自动化。以前为了对齐父子视图的圆角,开发者得手动算 padding 或者套 mask,现在 macOS 27 直接给了 NSViewCornerConfiguration API,让子视图自动贴合父视图的圆角。加上液态玻璃(Liquid Glass)材质的自动演进,AppKit 在视觉和交互上终于向现代声明式框架看齐了。
这对还在维护大型纯 AppKit 项目,或者搞 AppKit + SwiftUI 混合架构的 macOS 开发者影响最大。如果你还在手写 tracking loop,或者手动计算圆角半径,是时候重构了。
值得深挖的点
输入模型大换血:为什么必须干掉 mouseDown:?
Apple 强推手势识别器和控制事件替代 mouseDown:,主要原因是跨框架的统一。现在 Mac 上跑着 AppKit、SwiftUI 和 Mac Catalyst (UIKit),如果 AppKit 还在用古老的追踪循环,当 SwiftUI 视图嵌入 AppKit 窗口时,手势和点击事件就会打架。
控制事件的引入非常实用。以前想知道一个按钮是“正在被按下”还是“按下后鼠标移出了按钮区域”,你得子类化 NSButton 然后重写一堆 mouseDown: 和 mouseExited:。现在直接给按钮加 .trackingEndedOutside 控制事件就行,连子类都不用写。
不过坑也很明显:手势识别器是挂在视图层级上的,如果你有一个覆盖在上面的透明兄弟视图,它会把鼠标事件悄悄吃掉。Apple 给的解法是重写覆盖层的 hitTest(_:) 返回 nil,让事件穿透。老项目迁移时,一定要全局排查这种“隐形遮挡”问题,否则你会发现很多按钮突然点不动了。
UI 细节自动化:边角同心性(Concentricity)是个神仙 API
macOS 的设计语言越来越依赖大圆角和复杂材质。在实际开发中,如果一个带圆角的父容器里放了一个带背景色的子视图,子视图的圆角必须比父视图小,且圆心必须一致(同心),否则边缘就会出现难看的白边或锯齿。
以前解决这个问题,要么手动计算 cornerRadius = parentRadius - padding,要么用 CAShapeLayer 做 mask,既繁琐又影响渲染性能。这次新增的 NSViewCornerConfiguration 提供了 .containerConcentric 选项,直接让系统接管计算。这是非常典型的“把复杂留给系统,把简单留给开发者”的设计。配合 Liquid Glass 在 macOS 27 的自动硬边效果和半粗体侧边栏选中态,AppKit 开发者终于不用在像素级对齐上浪费生命了。
代码片段
1. 用控制事件替代鼠标追踪
场景:监听按钮的复杂交互状态,无需子类化 NSButton。
let button = NSButton()
// 监听鼠标在按钮外部抬起的事件,替代繁琐的 mouseExited 重写
button.addTarget(
self,
action: #selector(trackingEndedOutsideHandler),
for: .trackingEndedOutside
)
坑:控制事件依赖标准的控件实现,如果你自定义的控件没有正确调用 sendAction,这些事件就不会触发。
2. 解决手势识别器的事件遮挡
场景:当视图层级中有覆盖层(Overlay)导致下方按钮无法点击时。
class OverlayView: NSView {
// 重写 hitTest 返回 nil,让鼠标事件穿透到下方的视图
override func hitTest(_ point: NSPoint) -> NSView? {
return nil
}
}
坑:返回 nil 意味着这个覆盖层自己也无法接收任何鼠标事件,确保它真的只是一个纯视觉层。
3. 状态栏自定义 UI 的焦点管理
场景:点击状态栏图标弹出自定义窗口时,让系统正确接管键盘焦点。
extension AppDelegate: NSStatusItemExpandedInterfaceDelegate {
// 系统准备展示扩展界面时调用
func statusItem(_ statusItem: NSStatusItem, didBegin session: NSStatusItemExpandedInterfaceSession) {
customWindow.makeKeyAndOrderFront(nil) // 显示自定义窗口
}
// 用户执行了操作,主动取消会话以关闭窗口
func selectedAction() {
statusItem.expandedInterfaceSession?.cancel()
}
}
坑:如果你不实现这个 Delegate,弹出的自定义窗口在键盘导航(Tab 键)时会导致焦点乱飞,甚至卡死在状态栏。
最佳实践
老项目迁移不要想着“一键替换”。先全局搜索 mouseDown: 和 mouseDragged:,把那些只为了处理“点击”和“拖拽”的代码换成 NSClickGestureRecognizer 和 NSPanGestureRecognizer。对于列表选择、右键菜单和拖放,直接用 AppKit 现成的基于视图的 API(如 menuForEvent: 和现代的 pasteboardWriterForRow),别再自己算坐标了。
新项目必须把 window.autorecalculatesKeyViewLoop = true 和 window.preventsApplicationTerminationWhenModal = false 作为 Window Controller 的标配。前者让 Tab 键导航自动适应视图增删,后者确保你的非关键 Sheet 不会在系统夜间重启更新时阻挡整个 Mac 关机。
状态恢复(State Restoration)千万不要把整个文档数据塞进 encodeRestorableState。只存 UI 状态,比如侧边栏选中的 productIdentifier。数据本身应该从数据库或文件系统里读,否则你的 App 恢复时会慢得让人想砸键盘。
还有什么值得关注
- NSTextSelectionManager:让非
NSTextView的自定义视图也能白嫖原生的双向文本选择、拖拽和切换行为。 - Liquid Glass 自动演进:macOS 27 会自动给侧边栏选中项加半粗体,给工具栏加玻璃材质,老 App 重新编译就能获得视觉升级。
- NSTableView 现代拖拽:用
pasteboardWriterForRow替代老旧的writeRowsWithIndexes,直接返回NSPasteboardItem,跨进程拖拽更稳。