在 2026 年使用 SwiftUI 构建纯正的 Mac 应用

Lobsters Hottest 工具

摘要

这篇文章讲述了作者完全使用 SwiftUI 构建 macOS 应用的经验,讨论了在实现原生 Mac 体验时遇到的挑战和限制,例如选中状态和非活动窗口的行为,并得出结论:SwiftUI 在 Mac 上尚不足以构建‘纯正 Mac’应用。

<p><a href="https://lobste.rs/s/kbaepn/using_swiftui_build_mac_assed_app_2026">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/26 11:19

# 在2026年用SwiftUI构建一个地道的Mac应用 来源:https://pfandrade.me/blog/mac-assed-swiftui-app/ 我最近发布了Shopie (https://shopie.io/)的macOS版本,这款应用我去年年底首次在iOS App Store上架。Shopie能帮你追踪感兴趣的商品:创建心愿单,并在商品价格、库存及其他信息发生变化时通知你。 与我的其他应用(通常混合使用AppKit或UIKit与SwiftUI)不同,Shopie完全使用SwiftUI构建。我希望保持这种方式,以便在iOS、iPadOS和macOS之间最大化代码复用。这篇文章探讨了在2026年的Mac上,SwiftUI究竟能带你走多远,尤其是如果你的目标是打造一款真正符合平台原生感的应用。这不是对macOS上SwiftUI的详尽评测,而仅仅是记录我在移植Shopie (https://shopie.io/)(一个相对较小的应用)并坚持100% SwiftUI过程中遇到的一些做法和问题。 如果你想看结论:我们还没到那一步。 ## 什么是“Mac-assed app”? “Mac-assed app”这个术语由Collin Donnell (https://collin.blog/)创造,经Brent Simmons (https://inessential.com/2020/03/19/proxyman.html)和John Gruber (https://daringfireball.net/linked/2020/03/20/mac-assed-mac-apps)推广。它描述的应用不仅是原生的,还采用了系统的控件和惯例,并与操作系统的功能完美集成。 我认为Secrets (https://secrets.app/)就是一个地道的Mac应用,而且为此感到自豪。它使用原生控件,外观优美,大量依赖菜单栏,包含丰富的键盘快捷键,支持多窗口,有工具提示和悬停状态,并采用了系统技术,如密码自动填充、AppleScript(用于控制其他应用 (https://secrets.app/blog/2017/06/secrets-loves-transmit/))、Safari应用扩展以及突然终止。 如果你是长期Mac用户,当一个应用满足这些条件时,你就能*感觉到*。但基于Electron的应用,甚至许多第一方应用设定的标准,可能让新用户很难理解这一点。 ## SwiftUI在macOS上的不足 在将Shopie (https://shopie.io/)移植到macOS的过程中,我遇到了一系列问题:从“这个本应更简单”到“这根本不可能实现”。 ### 选中状态 在Mac上,选择是有细微差别的。一个项目可以在非活跃窗口中被选中,可以在视图失去焦点后仍被选中,也可以根本没有被选中但仍作为当前上下文菜单的目标。SwiftUI处理了其中一部分,有些处理得很笨拙,有些则根本无法处理。 - **非活跃窗口** 当前的HIG (https://developer.apple.com/design/human-interface-guidelines/windows#macOS-window-states)规定,非活跃窗口应“显得柔和,在视觉上比主窗口和关键窗口更远”。长期Mac用户知道这通常意味着更具体的东西。HIG的旧版本 (https://github.com/gingerbeardman/apple-human-interface-guidelines)明确指出了这一点:“只有关键窗口的控件才有颜色”。Finder中的活跃与非活跃窗口 Finder中的活跃与非活跃窗口 忽略这一点通常是应用由Electron制作的第一个迹象。我写这篇文章的Visual Studio Code就没有遵循这个惯例。这部分在SwiftUI中其实没问题。就像在AppKit中一样,许多系统控件如`List`和`Button`会自动处理,自定义控件可以通过检查`\.appearsActive`↗︎ (https://developer.apple.com/documentation/swiftui/environmentvalues/appearsactive)来实现相同效果。 - **选中但未聚焦** 接下来是项目仍被选中,但其视图已失去焦点的情况。Mail中的弱化选择 Mail中的弱化选择 这一点很重要,因为焦点告诉用户UI的哪个部分会对键盘输入做出响应。在上面的截图中,一封邮件被选中,但包含它的列表并未获得焦点,因此按箭头键不会移动选择。AppKit内置了答案:`NSTableRowView`暴露了`isEmphasized`↗︎ (https://developer.apple.com/documentation/appkit/nstablerowview/isemphasized),允许你在焦点移到别处时调整选择外观。在SwiftUI中,如果你使用`ScrollView`和`LazyVStack`而不是`List`来自建列表,你可以通过跟踪滚动视图的焦点状态并将其通过环境传递下去来实现相同行为。```swift ScrollView { LazyVStack { // 内容 } } .focusable(true) .focused($isScrollViewFocused) .environment(\.isEmphasized, isScrollViewFocused) ``` 行视图随后可以同时读取`\.isEmphasized`和`\.appearsActive`,并相应调整其选择样式。 - **上下文菜单目标** 不可能实现的情况是上下文菜单。在一个地道的Mac应用中,打开上下文菜单时,应在菜单所应用的项目周围显示焦点环,即使该项目未被选中。macOS上Reminders中的上下文菜单目标与选中项对比 macOS上Reminders中的上下文菜单目标与选中项对比 在上面的截图中,菜单应用于“购物”列表,尽管“提醒事项”仍被选中,并且界面清晰地表明了这种区别。macOS上Stocks中的上下文菜单目标与选中项对比 macOS上Stocks中的上下文菜单目标与选中项对比 例如在Stocks中,菜单应用于“AAPL”而不是当前选中的“MSFT”股票,但界面并未传达这一点。Notes应用则在错误方向上更进一步:右键点击未选中的笔记会立即更改选中项。这非常不像是地道的Mac行为 😪。Reminders、Notes和Stocks都是macOS上的SwiftUI应用,但每个行为都不同。Reminders之所以正确,是因为它使用了`List`,继承了`NSTableView`的行为。但一旦离开`List`,你就卡住了。五年多过去了,SwiftUI仍然无法让你知道上下文菜单是否打开。如果你无法知道这一点,就无法相应地调整UI。我猜测这是因为在iOS上这几乎没什么影响,系统在上下文菜单出现时会自动*抬高*相关元素。iOS上的上下文菜单 iOS上的上下文菜单 然而在macOS上,这个遗漏就显得很刺眼。这也清楚地表明了如今Mac在苹果内部似乎有多重要。 在继续之前,值得提一下SwiftUI的`List`。你可能已经注意到它几乎免费提供了上述所有行为。 但问题是`List`极难自定义。仅仅是使用`\.listRowBackground()`↗︎ (https://developer.apple.com/documentation/swiftui/view/listrowbackground(_:))更改选择颜色就可能破坏选择淡出动画,而且根本没办法自定义内置的上下文菜单焦点环。 但这本不必如此。如果你熟悉`UITableViewCell`、`UICollectionViewCell`或`NSTableRowView`,你可能也会好奇为什么`List`不简单地将`\.isHighlighted`、`\.isSelected`和`\.isEmphasized`等值传递给它的行。这将使其有用得多。 ### 拖放 拖放是Mac体验的核心。它让平台感觉直接而触手可及:将文件拖入应用、重新排列列表、将标签页拖入新窗口、将图像拖入文本字段。所以,SwiftUI在这方面仍感觉不稳定,这有点让人不安。 事实上,SwiftUI已经经历了三个拖放时代。它始于`onDrag(_:)`↗︎ (https://developer.apple.com/documentation/swiftui/view/ondrag(_:))和`onDrop(...)`↗︎ (https://developer.apple.com/documentation/swiftui/view/ondrop(of:istargeted:perform:)),这是一个围绕`NSItemProvider`构建的API。 然后,在iOS 16和macOS 13中,苹果引入了`Transferable`来取代`NSItemProvider`,以及新的`draggable(_:)`↗︎ (https://developer.apple.com/documentation/swiftui/view/draggable(_:))和`dropDestination(for:action:isTargeted:)`↗︎ (https://developer.apple.com/documentation/swiftui/view/dropdestination(for:action:istargeted:))API。 最后,iOS 26和macOS 26引入了第三次迭代:一个新的`dropDestination(for:isEnabled:action:)`↗︎ (https://developer.apple.com/documentation/swiftui/view/dropdestination(for:isenabled:action:))重载,它接受一个`DropSession`,以及用于多项目拖动的新的基于容器的拖放API。 但*所有三个*的问题在于,除非你是放置目标,否则你无法看到拖放会话内部。当你开始拖动一个UI元素时,你可能希望将其变暗,甚至在拖动过程中从界面中移除它。在iOS上可以找到这两种行为的例子。 在iOS上拖动Reminders中的列表会在拖动时将其移除 在iOS上拖动Reminders中的列表会在拖动时将其移除 但这在SwiftUI中无法正确实现。你可能想用`.onDrag()`来使视图变暗,但如果用户将项目放到你的窗口之外,你无法知道这件事,导致项目卡在变暗状态。 相比之下,AppKit的`NSDraggingSource`↗︎ (https://developer.apple.com/documentation/appkit/nsdraggingsource)从一开始就提供了你需要的所有信息。为什么SwiftUI仍然没有,这让我无法理解。 ### 键盘快捷键 你通常可以通过他们使用键盘的程度来判断一个Mac高级用户和普通用户。他们不仅仅使用`⌘C`和`⌘V`。他们用箭头键浏览列表,在窗格之间跳转,从菜单中触发命令而不动鼠标,并且通常期望应用能跟上。 SwiftUI可以支持这一点,但感觉仍比应该的要麻烦。一个很好的例子是箭头键。在macOS上,你应该使用`.onMoveCommand`↗︎ (https://developer.apple.com/documentation/swiftui/view/onmovecommand(perform:))。但它仍然在iOS上不可用,即使iPad应用经常配合也有箭头键的硬件键盘使用🤷。 所以你最终要为macOS写一套代码路径,为iPadOS写另一套,而本质上这是完全相同的用户交互。这种平台分裂让SwiftUI感觉不像它被承诺的统一UI框架。 一旦`TextField`获得焦点,情况就更糟了。此时它会愉快地吞噬键盘事件,几乎不给你参与的空间。一个好的例子是搜索。Spotlight允许你在搜索字段中继续输入的同时,使用上下箭头键在结果中移动。这是完全标准的Mac交互,我在Secrets (https://secrets.app/)的第一个版本(10年前)中就已经实现了。可惜,在纯SwiftUI中目前还无法实现。 再说一次,问题不在于SwiftUI中键盘支持不可能。而在于框架只给了你足够覆盖简单情况的能力,然后当你尝试匹配Mac应用已经做了几十年的事情时,它就开始阻碍你。 ### 窗口工具栏项目 工具栏是另一个SwiftUI在iPhone和iPad上感觉比Mac上舒服得多的领域。在macOS上,工具栏布局很重要。用户会对操作位置以及哪些项目属于侧边栏与详细视图形成肌肉记忆。 在三窗格分割视图中,这尤其尴尬。SwiftUI要求你使用语义描述工具栏项目,例如`.primaryAction`、`.secondaryAction`和`.navigation`。理论上听起来不错,但实际上这些放置位置在不同平台上含义不同,甚至在macOS上也很难预测一个项目最终会出现在哪里。 更糟糕的是,工具栏实际上是通过从视图层次结构中收集`.toolbar`修饰符为你组装的。这对于简单屏幕很方便,但一旦你需要精确性,就会感到抓狂。你常常不是在设计一个连贯的Mac工具栏,而是在与SwiftUI对你层次结构的解释做谈判,并希望最终布局符合你的想法。 再说一次,问题不在于SwiftUI让工具栏无法实现。而在于框架恰恰抽象掉了当你试图让Mac应用感觉刻意时最重要的那些部分。 ## 地道Mac应用的衰落 曾几何时,Mac应用毫不掩饰地充满Mac特色。Panic、Omni、Cultured Code、Bare Bones、Sofa。iPhone SDK发布前的几年可能是地道Mac应用的巅峰。然后苹果的重心转向了iPhone。 现在我们有Electron、Catalyst和iPadOS应用在Mac上运行。甚至苹果自己的SwiftUI应用也常常抹去了最初让Mac软件感觉很棒的那些行为。 回顾过去的苹果设计大奖,2018年的Agenda可能真的是最后一个真正地道的Mac获奖者。这很能说明问题。 苹果在这里失职了。AppKit领先于时代,而UIKit是更精致的AppKit。一个统一两者的严肃跨平台框架本应在SwiftUI之前很久就实现。相反,苹果让AppKit僵化,然后试图跳过问题。 你在各处都能看到结果。SwiftUI高效、现代,常常令人愉悦,直到你试图做一个真正优秀的Mac应用。然后你突然发现自己为了Mac 20年前就解决的问题而与框架斗争。

相似文章

全程原生,直到你需要文本

Hacker News Top

一位资深 macOS/iOS 开发者讲述了使用苹果原生框架(SwiftUI、AppKit、TextKit)实现支持 Markdown 的聊天界面的挣扎,最终发现像 Electron 这样的基于 Web 的技术为富文本渲染提供了更实用的解决方案。

引用约翰·格鲁伯

Simon Willison's Blog

约翰·格鲁伯讨论了苹果的竞争优势正在减弱,因为越来越少的开发者有动力为苹果平台打造独家的高质量原生应用,导致第三方软件质量向行业平均水平回归。