Using SwiftUI to Build a Mac-assed App in 2026
Using SwiftUI to Build a Mac-assed App in 2026
2026 年,使用 SwiftUI 构建“纯正 Mac 应用”
I recently launched the macOS version of Shopie, an app I first released on the iOS App Store late last year. Shopie helps you keep track of products you’re interested in by letting you create wishlists and notifying you whenever a product’s price, availability, and other details change. 我最近发布了 Shopie 的 macOS 版本,这款应用最初于去年年底在 iOS App Store 上线。Shopie 可以让你创建愿望清单,并在产品价格、库存或其他详细信息发生变化时通知你,从而帮助你跟踪感兴趣的商品。
Unlike my other apps, where I typically blend AppKit (or UIKit) with SwiftUI, Shopie is built entirely in SwiftUI. I wanted to keep it that way to maximize code reuse across iOS, iPadOS, and now macOS. This post explores how far SwiftUI can take you on the Mac in 2026, especially if your goal is to build an app that feels truly native to the platform. 与我通常将 AppKit(或 UIKit)与 SwiftUI 混合使用的其他应用不同,Shopie 完全是用 SwiftUI 构建的。我希望保持这种方式,以最大限度地提高 iOS、iPadOS 以及现在的 macOS 之间的代码复用率。本文探讨了在 2026 年,SwiftUI 在 Mac 上能达到什么程度,特别是当你的目标是构建一款感觉真正原生于该平台应用的时候。
It’s not meant to be an exhaustive review of SwiftUI on macOS. It’s simply a collection of recipes and issues I ran into while porting Shopie, a fairly small app, and keeping it 100% SwiftUI. If you want the TL;DR: we’re not there yet. 本文并非对 macOS 上 SwiftUI 的详尽评测,而仅仅是我在将 Shopie(一款相当小的应用)移植并保持 100% SwiftUI 过程中所遇到的技巧和问题的汇总。如果你想要结论:我们还没到那一步。
What’s a Mac-assed app? The term “Mac-assed app” was coined by Collin Donnell and popularized by Brent Simmons and John Gruber. It describes apps that are not only native, but that also adopt the system’s controls and conventions and integrate impeccably with the operating system’s features. 什么是“纯正 Mac 应用”(Mac-assed app)?“Mac-assed app”这个词由 Collin Donnell 创造,并经由 Brent Simmons 和 John Gruber 发扬光大。它描述的不仅仅是原生应用,还包括那些采用了系统控件和惯例,并与操作系统功能完美集成的应用。
I consider Secrets to be a Mac-assed app, and proudly so. It uses native controls and looks beautiful while doing so. It leans heavily on the menu bar, includes plenty of keyboard shortcuts, supports multiple windows, has tooltips and hover states, and adopts system technologies such as Password AutoFill, AppleScript (to control other apps), Safari app extensions, and sudden termination. 我认为 Secrets 是一款纯正的 Mac 应用,并为此感到自豪。它使用原生控件,且外观精美。它深度依赖菜单栏,包含大量键盘快捷键,支持多窗口,拥有工具提示和悬停状态,并采用了密码自动填充、AppleScript(用于控制其他应用)、Safari 应用扩展和“突然终止”(sudden termination)等系统技术。
If you’re a long-time Mac user, you can just feel it when an app ticks these boxes. But the popularity of Electron-based apps, and even the standard set by many first-party apps, may make this much harder for newer users to understand. 如果你是一位资深的 Mac 用户,当一款应用满足这些条件时,你会有直观的感受。但基于 Electron 的应用的流行,甚至许多第一方应用所设定的标准,可能让新用户更难理解这一点。
SwiftUI shortcomings on macOS
SwiftUI 在 macOS 上的不足
While porting Shopie to macOS, I ran into a spectrum of problems: from “this should be easier” to “this is simply not possible.” 在将 Shopie 移植到 macOS 的过程中,我遇到了一系列问题:从“这应该更容易实现”到“这根本不可能实现”。
Selected states
选中状态
On the Mac, selection has nuance. An item can be selected in an inactive window, selected but in a view that no longer has focus, or not selected at all and still be the current context menu target. SwiftUI handles some of this well, some of it awkwardly, and some of it not at all. 在 Mac 上,选中状态是有细微差别的。一个项目可以在非活动窗口中被选中,可以在失去焦点的视图中被选中,或者根本没有被选中但仍然是当前上下文菜单的目标。SwiftUI 对其中一些处理得很好,有些处理得很笨拙,而有些则完全无法处理。
Inactive windows
非活动窗口
The current HIG says inactive windows should “appear subdued and seem visually farther away than the main and key windows.” Long-time Mac users know that usually means something more specific. Older versions of the HIG spelled this out explicitly: “only the controls of the key window have color.” 当前的 HIG(人机交互指南)指出,非活动窗口应该“显得柔和,在视觉上看起来比主窗口和关键窗口更远”。资深 Mac 用户知道,这通常意味着更具体的内容。旧版本的 HIG 明确指出:“只有关键窗口的控件才有颜色。”
Ignoring this is often the first tell that an app was made with Electron. Visual Studio Code, where I’m writing this, doesn’t follow this convention. This part is actually fine in SwiftUI. Just like in AppKit, many system controls such as List and Button get it automatically, and custom controls can do the same by checking \.appearsActive.
忽略这一点通常是判断一款应用是否由 Electron 构建的首要标志。我正在用来写作的 Visual Studio Code 就没有遵循这一惯例。在 SwiftUI 中,这部分其实处理得很好。就像在 AppKit 中一样,许多系统控件(如 List 和 Button)会自动处理,自定义控件也可以通过检查 \.appearsActive 来实现同样的效果。
Selected but not focused
选中但未聚焦
Next comes the case where an item is still selected, but its view is no longer focused. This matters because focus tells the user which part of the UI will respond to keyboard input. In the screenshot above, an email is selected but the containing list is not focused, so pressing the arrow keys won’t move that selection. 接下来是这种情况:项目仍然被选中,但其视图不再处于焦点状态。这很重要,因为焦点告诉用户界面的哪一部分将响应键盘输入。在上面的截图中,一封邮件被选中,但包含它的列表没有焦点,因此按下箭头键不会移动该选中项。
AppKit has a built-in answer here: NSTableRowView exposes isEmphasized, which lets you adjust the selection appearance when focus moves elsewhere. In SwiftUI, if you’re building your own list with ScrollView and LazyVStack instead of using List, you can achieve the same behavior by tracking the scroll view’s focus state and passing that down through the environment.
AppKit 在这里有一个内置的解决方案:NSTableRowView 暴露了 isEmphasized 属性,让你可以在焦点移到别处时调整选中项的外观。在 SwiftUI 中,如果你使用 ScrollView 和 LazyVStack 构建自己的列表而不是使用 List,你可以通过跟踪滚动视图的焦点状态并通过环境(environment)向下传递来实现相同的行为。
ScrollView {
LazyVStack {
// content
}
}
.focusable(true)
.focused($isScrollViewFocused)
.environment(\.isEmphasized, isScrollViewFocused)
The rows can then read both \.isEmphasized and \.appearsActive and adjust their selection styling accordingly.
然后,行视图可以同时读取 \.isEmphasized 和 \.appearsActive,并相应地调整其选中样式。
Context menu targets
上下文菜单目标
The impossible case is context menus. In a proper Mac-assed app, opening a context menu should enable a focus ring around the item the menu applies to, even when that item isn’t selected. 最棘手的情况是上下文菜单。在一款纯正的 Mac 应用中,打开上下文菜单时,即使该项目未被选中,也应该在菜单所应用的项目周围显示焦点环。
In the screenshot above, the menu applies to the “Shopping” list even though “Reminders” is still selected, and the UI makes that distinction clear. In Stocks, for example, the menu applies to “AAPL” rather than the currently selected “MSFT” stock, but the interface doesn’t communicate that. The Notes app goes a step further in the wrong direction: right-clicking an unselected note immediately changes the selection. That is very much not a Mac-assed behavior 😪. 在上面的截图中,尽管“Reminders”仍然被选中,但菜单应用于“Shopping”列表,界面清晰地做出了这种区分。例如,在“股票”应用中,菜单应用于“AAPL”而不是当前选中的“MSFT”股票,但界面并没有传达这一点。“备忘录”应用则在错误的方向上走得更远:右键点击一个未选中的笔记会立即改变选中项。这绝对不是一种纯正的 Mac 行为 😪。
Reminders, Notes, and Stocks are all SwiftUI apps on macOS, yet each behaves differently. Reminders only gets this right because it’s using List, which inherits the behavior from NSTableView. Step outside List, though, and you’re stuck.
“提醒事项”、“备忘录”和“股票”都是 macOS 上的 SwiftUI 应用,但它们的行为各不相同。“提醒事项”之所以处理得当,是因为它使用了 List,而 List 从 NSTableView 继承了这种行为。然而,一旦跳出 List,你就束手无策了。
More than five years in, SwiftUI still gives you no way to know whether a context menu is open. And if you can’t know that, you can’t adjust your UI accordingly. My guess is that this fell through the cracks because it barely matters on iOS, where the system automatically elevates the relevant element when a context menu appears. 五年多过去了,SwiftUI 仍然没有提供任何方法来获知上下文菜单是否已打开。如果你无法获知这一点,就无法相应地调整 UI。我猜这是因为在 iOS 上这几乎无关紧要,因为当上下文菜单出现时,系统会自动提升相关元素,所以这个问题被忽略了。
On macOS, though, the omission feels glaring. It’s also a telling sign of how much the Mac seems to matter inside Apple these days. Before moving on, it’s worth calling out SwiftUI’s List. You may have noticed that it gives you almost all of the behavior above for free. The catch is that List is incredibly hard to customize. Simply changing the selection color with .listRowBackground() can break the selection fade-out animation, and there’s no way to customize the built-in context menu focus ring at all.
但在 macOS 上,这种缺失显得非常刺眼。这也说明了如今 Mac 在苹果内部的受重视程度。在继续之前,值得一提的是 SwiftUI 的 List。你可能已经注意到,它几乎免费提供了上述所有行为。问题在于 List 非常难以定制。仅仅使用 .listRowBackground() 更改选中颜色就可能破坏选中项的淡出动画,而且根本无法自定义内置的上下文菜单焦点环。