复刻 macOS 红绿灯悬停菜单

复刻 macOS 红绿灯悬停菜单

💡 原文中文,约7300字,阅读约需18分钟。
📝

内容提要

文章介绍如何在macOS Tahoe中实现窗口平铺悬停菜单。作者通过逆向AppKit,发现该菜单是私有NSMenu,由XPC服务渲染。最终用十行代码调用私有方法`_hoverTriggeredShowMenuForView:`,复用系统原生功能,实现与真绿钮一致的宫格菜单,并附完整Swift代码示例。

🔎

延伸解读

逆向思路:先枚举,后反编译

文章展示了 macOS UI 逆向的实用路径:利用 Objective-C 运行时枚举类、方法和实例变量,再结合 dyld shared cache 的字符串扫描,快速定位私有 API。这种方法比直接反编译更高效,尤其适合探索系统 UI 组件的内部实现。对于开发者而言,掌握这种思路有助于理解系统行为,甚至发现可复用的私有接口。

私有 API 的代价与防御

使用私有 API 虽然能复用系统原生功能,但存在脆弱性:selector 可能改名,行为可能随系统版本变化。文章建议通过 responds(to:) 检查并准备降级路径来防御。开发者应权衡利弊,在追求一致性和维护成本之间做出选择。若私有接口失效,可重新运行探针脚本定位新入口,但需注意 App Store 审核风险。

自绘 vs 复用系统:兼容性考量

文章对比了自绘 UI 与复用系统私有实现的差异。自绘虽然可控,但难以完美复刻系统细节,且需持续跟进系统更新。复用系统代码则能保证像素级一致,但依赖私有 API。对于追求长期维护的开发者,自绘可能更稳妥;而追求短期效果或内部工具,复用系统入口更高效。

Q&A

macOS Tahoe 中窗口平铺悬停菜单的触发机制是什么?

在 macOS Tahoe 中,鼠标悬停在窗口左上角的绿色按钮(红绿灯按钮)上稍候片刻,会弹出带箭头的窗口平铺菜单。该菜单是一个私有 NSMenu,由 AppKit 内的 XPC 服务渲染。触发入口是私有方法 `_hoverTriggeredShowMenuForView:`,调用该方法即可复用系统原生功能。

如何在自己的 macOS 应用中实现类似 macOS Tahoe 的悬停平铺菜单?

可以通过调用私有方法 `_hoverTriggeredShowMenuForView:` 来实现。具体步骤:获取窗口的绿色按钮的 cell(通过 `window?.standardWindowButton(.zoomButton)?.cell`),然后检查该 cell 是否响应 `_hoverTriggeredShowMenuForView:` 选择器,若响应则调用它,并传入自定义视图作为锚点。同时需要设置 `NSZoomButtonMenuOutOfProcess` 为 false 以使用进程内渲染。

macOS 悬停菜单的私有实现中,有哪些相关的类和方法?

相关类包括 `_NSThemeZoomWidgetCell`(绿色按钮的 cell)、`NSThemeWidgetZoomMenuRemoteView`(负责将菜单项脱水后交给 XPC 服务)、`ThemeWidgetControlViewService.xpc` 中的 `NSThemeWidgetZoomMenuServiceViewController`(负责渲染)。相关方法包括 `_hoverTriggeredShowMenuForView:`(触发菜单)、`_windowTilingMenu`(获取菜单)、`-dehydrateMenuItems:`(脱水菜单项)、`-serviceSelectedMenuItemWithUUID:`(回传选中项)等。

macOS 悬停菜单的菜单结构是怎样的?

菜单结构包含节标题、正项和备选项。例如,'Move & Resize' 节包含节标题(不可点)、正项(挂方位子菜单,如 Left、Right、Top、Bottom)和备选项(按住 Option 时替换正项,挂四象限子菜单)。'Fill & Arrange' 节类似。底部还有 Full Screen 项。

macOS 悬停菜单有哪些相关的 UserDefaults 键?

相关的 UserDefaults 键包括:`NSZoomButtonMenuOption`(总开关,置 0 禁用)、`NSZoomButtonMenuHoverTimeout`(悬停多久弹出)、`NSZoomButtonDragHandoffTimeout`(按住拖离按钮的交接延时)、`NSZoomButtonMenuOutOfProcess`(切换进程外与进程内渲染)、`NSZoomButtonServiceWarmUpDelay`(XPC 服务预热时机)。

使用私有 API 实现悬停菜单有哪些风险和注意事项?

私有 API 的代价是脆弱:selector 可能改名,行为可能有 SDK 版本门槛。防御措施包括使用 `responds(to:)` 检查并准备降级路径。如果入口被封死,可以重新运行探针脚本定位新的方法名。

为什么自绘菜单不如调用系统私有方法?

自绘菜单需要自己实现外观和交互,难以完全匹配系统原生效果,且需要不断适配系统更新。而调用私有方法可以复用系统原生渲染,保证与真绿钮一致的宫格菜单、动画和子菜单,且代码量少(十行左右)。

🏷️

标签

➡️

继续阅读