资讯动态

OpenDisplay AppPresentation状态机:菜单栏模式与窗口模式的设计取舍

发布时间:2026/9/18 19:57:07 来源:尧图企业网站定制
OpenDisplay AppPresentation状态机菜单栏模式与窗口模式的设计取舍【免费下载链接】opendisplayFree, open-source Sidecar/Duet alternative — use your iPhone, iPad or Mac as a true second monitor for your primary Mac over USB or WiFi. Low latency H.264, Retina HiDPI, touch input.项目地址: https://gitcode.com/GitHub_Trending/op/opendisplayOpenDisplay 是一个免费的开源第二屏虚拟显示器工具可以把 iPhone、iPad 或备用 Mac 变成主 Mac 的真实扩展显示器。本文深入解析它的 Mac 端核心设计——AppPresentation状态机一个枚举、三种出现方式菜单栏、Dock 窗口、纯后台以及背后关于激活策略、单一事实来源和运行时热切换的完整取舍。为什么第二屏工具需要三种出现方式OpenDisplay 的 Mac 端程序长期运行在后台它创建虚拟显示器、用 VideoToolbox 硬件编码 H.264再通过 USB 或 WiFi 把画面流式推送到设备。对这类跑起来就不用管的工具用户的使用习惯差异很大有人希望它像剪贴板工具一样只占菜单栏一小格不打扰 Dock有人习惯正常窗口应用想从 Dock 点图标打开控制面板也有人要完全隐形流媒体继续跑但屏幕上什么都不留。OpenDisplay 的解法很巧妙不打包三个 App而是一个 bundle 在运行时切换外观走的是 Raycast / Hammerspoon 风格的后台代理background agent路线。这个决策的入口就是下面这个状态机。AppPresentation 状态机三个状态一个单一事实来源状态机的全部定义在 OpenSidecarMacApp.swift 中短到一眼能看完enum AppPresentation: String, CaseIterable { case menuBar, dock, background }三个状态各有中文名与用途状态界面上显示激活策略典型场景menuBar菜单栏菜单栏图标 下拉面板.accessory无 Dock 图标日常默认最省资源dock窗口独立控制面板窗口.regular有 Dock 图标偏好传统 App 体验background纯后台无任何界面.accessory挂机投屏、专用投屏机状态机的心脏是控制器里的一个带副作用的发布属性 SenderControllerPublished var presentation AppPresentation( rawValue: UserDefaults.standard.string(forKey: presentation) ?? ) ?? .menuBar { didSet { UserDefaults.standard.set(presentation.rawValue, forKey: presentation) NSApp.setActivationPolicy(presentation .dock ? .regular : .accessory) // Never strand the user without UI if presentation ! .menuBar { MainWindow.show() } } }这段代码是全文最值得初学者借鉴的设计它把三件事收敛在一处持久化选择写入UserDefaults下次启动自动恢复系统行为切换通过NSApp.setActivationPolicy决定 Dock 图标显隐——只有dock状态用.regular其余都是.accessory兜底规则离开菜单栏模式时立即弹出控制窗口永远不让用户找不到界面。这就是状态机里最重要的一个不变量UI 入口和状态永远同步切换是热切换不需要重启 App。菜单栏模式MenuBarExtra 的只读插入绑定菜单栏模式是默认值它的实现有一个反直觉的细节。整个 App 只有一个 SwiftUI Scene MenuBarExtraMenuBarExtra(isInserted: Binding( get: { controller.presentation .menuBar }, set: { _ in } // 故意不处理写入 )) { ... }注意set是空实现——菜单栏图标的插入与否完全由状态机推导而非用户动作。图标本身还会随投屏状态变化运行时是实心图标rectangle.on.rectangle.fill空闲时是空心扫一眼菜单栏就知道第二屏有没有在工作。面板内容用的是.window风格而非原生菜单因为控制面板里有表单、按钮等富交互。代码里还记录了一个真实的坑MenuBarExtra窗口对分组表单的自动测量不可靠在高分屏上会裁剪所以面板显式固定为 440×540 并内部滚动面板尺寸说明。窗口模式与纯后台模式给不想被图标打扰的人Dock 窗口模式的承载者是 MainWindow——一个脱离 SwiftUI Scene、直接用 AppKit 构建的NSWindow内容用NSHostingView包同一个ContentView。这是一个 bundle 两种皮肤的关键菜单栏面板和 Dock 窗口共享同一份控制器、同一个 Sparkle 更新器实例只是挂在不同的宿主上。纯后台模式则把 UI 完全关掉但流媒体管线照跑。为了让它不至于变成打不开的死胡同App 重开了 applicationShouldHandleReopenBackground/Dock 模式下再次打开 AppSpotlight、Finder、Dock 点击都会带出控制窗口——Hammerspoon 风格。界面里也配了诚实的提示文字选择Background only后会说明无菜单栏和 Dock 图标但投屏继续运行再次打开 OpenDisplay 即可显示此窗口 界面文案。设置入口本身则是一个极简的PickerShow app in…三个选项直接遍历AppPresentation.allCases呈现方式选择器。设计取舍总结为什么是一个 bundle而不是两个 App把这套状态机摊开看每个选择背后都是一次明确的取舍单一 bundle 运行时切换vs 双 App 分发只有一份代码、一个更新器、一份UserDefaults不存在菜单栏版和窗口版版本不一致的漂移问题代价是启动时要先读状态再决定激活策略 启动时决策。状态驱动 UIvs 用户驱动 UI菜单栏图标的显隐是状态的派生结果切换发生在didSet里同步完成用户永远看不到图标没了但窗口也没了的中间态。.accessory兜底两种无 Dock 模式共用.accessory策略保证菜单栏模式和纯后台模式的系统占用完全一致区别只是面板插没插。这套菜单栏 ↔ 窗口 ↔ 纯后台的三态设计是第二屏工具类 App 很值得参考的通用方案后台代理的本质是长期驻留而驻留型应用的 UI 形态必须服从用户的工作流而不是反过来。OpenDisplay 用一个 11 行的枚举 一个带didSet的属性就实现了完整、可持久化、可热切换的呈现状态机。相关模块与源码位置状态机与 App 入口Mac/OpenSidecarMacApp.swift投屏管线虚拟显示器 编码器 传输Mac/MacSender.swift、Mac/VirtualDisplay.swift接收端备用 Mac 作为显示器MacReceiver/MacReceiver.swift、MacReceiver/ReceiverPanel.swift通信协议说明PROTOCOL.md功能历史v0.3.0 引入菜单栏 App 与呈现模式CHANGELOG.md【免费下载链接】opendisplayFree, open-source Sidecar/Duet alternative — use your iPhone, iPad or Mac as a true second monitor for your primary Mac over USB or WiFi. Low latency H.264, Retina HiDPI, touch input.项目地址: https://gitcode.com/GitHub_Trending/op/opendisplay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价