Ice 深度解析:macOS 菜单栏管理工具的完全实战指南
【免费下载链接】IcePowerful menu bar manager for macOS项目地址: https://gitcode.com/GitHub_Trending/ice/Ice
菜单栏图标一多,加上刘海屏挤压,macOS 的菜单栏就经常处于“溢出”状态。Ice 是一款面向 macOS 14+ 的菜单栏管理工具(GPL-3.0 开源):三段分区、图标隐藏与自动还原、拖拽重排、外观定制,一次打包。本文基于仓库源码,拆解它如何在不动用任何图标移动 API 的前提下“管理别人家的图标”。
项目速览
Ice 的定位是“尽可能全面的菜单栏管理器”,核心能力包括:
- 隐藏/显示菜单栏图标,含一个“始终隐藏”区域
- 多种触发方式:悬停、点击空白处、滚动/滑动、全局快捷键
- 自动还原(rehide)策略,含“聚焦应用切换时还原”模式
- 拖拽重排图标(BETA)、刘海屏可用的第二条栏(Ice Bar)
- 菜单栏着色(纯色/渐变)、阴影、边框、自定义圆角与分隔形状
- 图标搜索、自动更新、登录自启动
安装只需一条brew install --cask jordanbaird-ice,或从 release 直接下载。
核心原理:它是怎么做到的
原理一:分区 + 宽度“挤占”,而不是移动图标
macOS 没有提供任何公开 API 让第三方移动别的 App 的NSStatusItem,Ice 的解法是换一个思路:自己在菜单栏里放三个NSStatusItem作为“分区分隔符”,把菜单栏划成 visible / hidden / alwaysHidden 三段,用宽度几何控制哪些区域可见。
关键实现在 ControlItem.swift:当分隔符处于“收起隐藏区”状态时,宽度被拉到 10_000pt,横向占满整条菜单栏,隐藏区图标被挤出可视范围;展开时缩回variableLength,图标重新回到可见区域:
statusItem.length = switch section.name { case .visible: Lengths.standard case .hidden, .alwaysHidden: switch state { case .hideItems: Lengths.expanded // 10_000 case .showItems: Lengths.standard } }设计动机:纯宽度操作不依赖私有接口,App 重启、系统重排菜单栏后行为依然成立。代价是 Ice 无法把某个具体图标挪到任意位置——FREQUENT_ISSUES.md 明确承认这一点,隐藏区图标的相对顺序仍由 macOS 决定。
原理二:窗口枚举 + 全局事件,搭出感知层
每个状态项本质上是一个顶层窗口,所以 MenuBarItem.swift 以 CGWindowList 的菜单栏窗口列表作为图标数据源,按显示器和 Space 过滤后排序:
static func getMenuBarItems(on display: CGDirectDisplayID? = nil, onScreenOnly: Bool, activeSpaceOnly: Bool) -> [MenuBarItem] { var option: Bridging.WindowListOption = [.menuBarItems] ... return Bridging.getWindowList(option: option).lazy .filter(boundsPredicate) .compactMap { MenuBarItem(windowID: $0) } .sortedByOrderInMenuBar() }在此基础上,MenuBarManager.swift 用辅助功能 API(AXSwift)读取应用菜单区 frame,与已显示图标对比,重叠时自动隐藏左侧应用菜单。事件侧由 Events/ 子系统的三层监听支撑:GlobalEventMonitor.swift(NSEvent 全局监听)、EventTap.swift(CGEventTap,用于识别菜单栏上的滚动/滑动手势)、LocalEventMonitor(应用内事件)。权衡在于:全局事件监听需要辅助功能权限,这是 Ice 唯一但也是最重的一次权限请求。
关键模块走读
- MenuBarManager.swift:中枢管理器,持有三个 section,同时观察系统菜单栏隐藏状态与前台应用变化,决定“何时该自动还原”。
- MenuBarSection.swift:section 状态机(show/hide/toggle),定时器 + 鼠标位置双重校验实现“离开菜单栏 N 秒后还原”,鼠标回到菜单栏会取消计时。
- ControlItem.swift:分区分隔符,实现万点宽度技巧、图标状态切换,并在移除前缓存
preferredPosition(原因见下节)。 - MenuBarItemManager.swift:图标缓存与拖拽重排(BETA),用 5 秒定时器 + 应用启动事件刷新缓存,避免高频枚举窗口。
- HotkeyRegistry.swift:全局快捷键统一注册,section 开关、搜索面板、Ice Bar 都从这里接线。
- PermissionsManager.swift:辅助功能权限申请与状态管理,避免重复弹窗。
- IceBar.swift:菜单栏下方第二条栏,专门解决刘海屏上隐藏区图标无处安放的问题。
- MenuBarSearchPanel.swift:图标搜索面板,把“找图标”从肉眼扫描变成键入过滤。
工程细节与值得借鉴的设计
系统缺陷的前后兜底:NSStatusBar有个坑——把 status item 从菜单栏移除会连带清掉它持久化的preferredPosition。Ice 在每次隐藏/移除前后手动读写缓存:
func removeFromMenuBar() { guard isAddedToMenuBar else { return } let autosaveName = statusItem.autosaveName as String let cached = StatusItemDefaults[.preferredPosition, autosaveName] statusItem.isVisible = false StatusItemDefaults[.preferredPosition, autosaveName] = cached }多屏 + 刘海特判:辅助功能 API 只返回当前活动屏幕的菜单栏,取副屏应用菜单 frame 时会错。MenuBarManager.swift 里专门对带刘海的屏幕做了宽度阈值判断,这类“注释解释为什么”的 workaround 在仓库里随处可见,是读这个代码库最有价值的部分。
状态驱动架构:几乎所有 UI 由 Combine 发布器驱动——@Published状态变化 → 宽度自动更新、设置变更 → 图标自动重绘、section 显隐 → 快捷键自动启用/停用。职责切分清晰:SettingsManagers 只存值,Manager 管逻辑,UI 只观察。代价是 Combine 样板代码偏多,新人上手有一定门槛。
版本取舍:只支持 macOS 14+,README 解释原因是用到了 14 起才有的系统 API——放弃旧版本换取实现简洁,对工具类 App 是合理决策。
边界与风险
- 不能移动图标:FAQ 明确 Ice 无法移动或删除图标。对不记忆图标位置的应用,系统会把它们重新插到最左侧(即 always-hidden 区),当前版本无解,只能手动拖回。
- 依赖系统行为的灰色地带:代码里移除零宽约束的 hack 自带注释“may break in a new macOS release”,大版本更新可能改变分隔符行为,属于持续维护成本。
- 系统功能互斥:开启“自动隐藏菜单栏”时无法排列图标,官方给出的解法是临时把控制中心设为 Never,操作完再改回。
- 权限:需要辅助功能权限用于事件监听与菜单区读取,首次配置要过一道授权。
- 功能边界:拖拽重排仍是 BETA;多布局 Profile、图标分组、条件显示等 roadmap 项均未实现,Ice/MenuBarItems 下的顺序记忆也依赖后续 issue 落地。
总结
Ice 的价值在于用“分区 + 宽度”这个纯几何方案,在不动任何图标移动 API 的前提下解决了 macOS 菜单栏溢出问题,且模块边界、注释质量在开源工具里属于高水准。它适合菜单栏图标多、或在使用刘海屏 MacBook 的 macOS 14+ 用户。后续看两点:图标顺序记忆和条件显示能否落地——那才是 Ice 天花板所在。
【免费下载链接】IcePowerful menu bar manager for macOS项目地址: https://gitcode.com/GitHub_Trending/ice/Ice
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考