Highlight
将 iOS 应用带到 Mac 有两条路径,今年都有新东西:
核心内容
很多 iOS app 已经能出现在 M1 Mac 的 App Store 里。只要没有主动退出,用户搜索时会在 “iPhone and iPad Apps” 标签下看到它们(03:09)。
这条路径成本最低。问题也很直接:触控输入、屏幕尺寸、全屏启动,都不一定像真正的 Mac app。
Apple 在这场 session 里把路径分成两条。
第一条,是继续把 iOS app 原生跑在 M1 Mac 上。开发者可以加两个 Info.plist key,让游戏和多媒体 app 使用真实屏幕尺寸,并在启动时直接进入全屏(03:29)。还可以用 Touch Alternatives,把键盘、鼠标和触控板输入转换成 iOS app 期待的多点触控手势(04:52)。
第二条,是把项目加上 Mac Catalyst destination。这样 app 可以跑在所有 Mac 上,并且能使用 Mac Catalyst API 做更深的定制(07:35)。如果选择 Optimize for Mac,UIKit 控件会变成 AppKit 风格,文本也会用 Mac 原生字体渲染(07:52)。
这不是一次“移植完成就结束”的工作。它更像一个阶梯:先让 app 出现在 Mac,再补输入和显示,再用 Catalyst 接住工具栏、窗口、菜单和弹出层这些桌面能力。
详细内容
1. 原生 iOS app:两个启动 key
(03:29)Apple 新增了两个可以放进 Info.plist 的 key。它们不绑定 SDK 版本,在 iOS 和 macOS 12.1 之前的 Mac 上会被忽略,所以可以安全加入已有 app。
<!-- Info.plist -->
<key>UISupportsTrueScreenSizeOnMac</key>
<true/>
<key>UILaunchToFullScreenByDefaultOnMac</key>
<true/>
关键点:
UISupportsTrueScreenSizeOnMac表示 app 已经准备好处理 Mac 上不同的显示器尺寸和像素密度。- 开启后,app 拿到真实屏幕尺寸和真实像素密度,而不是兼容用的 iPad 尺寸。
UILaunchToFullScreenByDefaultOnMac表示 app 希望启动后立刻进入全屏。- 两个 key 适合一起使用。视频里明确提到游戏和多媒体 app 可以因此获得 pixel-perfect、edge-to-edge 的全屏体验。
这类改动不要求你改 UI 架构。它解决的是“画面被放进一个兼容窗口里”的问题。
2. Touch Alternatives:用 plist 描述输入转换
(04:52)iOS 游戏常常只处理触控和设备运动。Mac 用户拿到的是键盘、鼠标和触控板。
Touch Alternatives 的作用,是把这些 Mac 输入自动转换成 iOS app 期待的多点触控手势和设备运动。
要开启自动 Touch Alternatives,创建一个名为 com.apple.uikit.inputalternatives.plist 的文件(05:35)。
<!-- com.apple.uikit.inputalternatives.plist -->
<key>defaultEnablement</key>
<string>enabled</string>
<key>requiredOnboarding</key>
<array>
<string>tap</string>
<string>tilt</string>
<string>drag</string>
<string>swipe</string>
</array>
关键点:
defaultEnablement设为enabled,表示 app 启动时立即打开 Touch Alternatives。requiredOnboarding是一个数组,用来列出你希望首次启动教程重点展示的控制方式。- 视频列出的可展示控制包括 tap、tilt、drag、swipe,以及来自触控板的 direct touch input。
- Touch Alternatives 一旦启用,所有控制方式都会生效;
requiredOnboarding只决定教程重点展示哪些。
(07:02)Apple 也强调,最好还是直接实现键盘和光标支持。这样 app 在带键盘的 iPad 和 Mac 上都能表现一致。Touch Alternatives 更适合作为现有触控 app 上 Mac 的桥接层。
3. Optimize for Mac:把 UIKit 结构翻译成 Mac 表示
(07:35)在 Xcode 里添加 Mac Catalyst destination 后,app 会变成完整的 Mac Catalyst app,可以运行在每一台 Mac 上。
如果启用 Optimize for Mac,系统会做几件事(08:18):
UINavigationBar -> NSToolbar
center item controls -> NSToolbarItems
UISearchTextField -> hosted inside an NSToolbarItem
iPad text rendering -> native Mac font rendering
关键点:
UINavigationBar会翻译成NSToolbar,并使用原生 AppKit 控件。- 中心区域的控件会变成
NSToolbarItem。 - 搜索栏会先显示为工具栏里的搜索按钮,点击后展开成搜索栏。
- 搜索建议菜单和搜索 scope bar 也会转换成原生 AppKit 控件。
- Mac idiom 下文本使用 Mac 原生字体渲染,视频称其为 pixel-perfect scale。
(09:49)如果你的 Catalyst app 没有自己创建工具栏,系统会自动给出一个。如果你已经自己管理 NSToolbar,系统会避开,不覆盖你的实现。
4. 文档菜单:用 responder chain 接住 File 菜单
(10:42)采用桌面级 iPad API 后,文档 app 会自动得到新的 File 菜单项:Duplicate、Move、Rename、Export As。
启用这些菜单项的条件,是 responder chain 里有对象覆盖 UIResponder 的 duplicate、move、rename 和 export functions(10:53)。
如果 app 不需要这些文档菜单项,可以在 app delegate 的 buildMenu 里用 UIMenuBuilder 移除。视频说明这些文档菜单项的 UIMenuIdentifier 值是 .document(11:07)。
func buildMenu(with builder: UIMenuBuilder) {
builder.remove(menu: .document)
}
关键点:
buildMenu(with:)是 app delegate 控制菜单的位置。UIMenuBuilder负责增删改系统菜单。.document指向这一组文档相关菜单项。- 如果 app 不提供对应文档操作,移除菜单比留下无效入口更符合 Mac 习惯。
5. 窗口几何:为辅助窗口设置尺寸和行为
(13:56)Catalyst app 已经用 UIWindowScene 支持多窗口。macOS Ventura 进一步加入了窗口 frame、窗口控制按钮和全屏行为的定制 API。
视频里的例子,是 Markdown app 打开一个 Markdown Hints 辅助窗口。这个窗口更小,而且禁用 minimize 和 zoom 按钮(14:22)。
下面是按 transcript 中 API 名称整理的概念代码,用来说明调用顺序:
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
var frame = windowScene.effectiveGeometry.systemFrame
frame.size = CGSize(width: 480, height: 360)
let preferences = UIWindowScene.GeometryPreferences.Mac(systemFrame: frame)
windowScene.requestGeometryUpdate(preferences) { error in
// The system can reject the request and report details here.
}
windowScene.windowingBehaviors.miniaturizable = false
windowScene.sizeRestrictions?.allowsFullScreen = false
}
关键点:
effectiveGeometry提供当前 frame;视频建议始终从它开始。- 在 scene 创建前,当前 frame 会初始化为
CGRectNull,系统知道在初次创建时忽略这些值。 - 修改
systemFrame后,通过requestGeometryUpdate()提交请求。 - 这是请求,不是命令;系统可以拒绝,并通过 error handler 返回细节。
windowingBehaviors.miniaturizable控制黄色 minimize 按钮。sizeRestrictions.allowsFullScreen控制绿色按钮进入全屏的能力。
(16:53)窗口几何还有两个尺度规则。systemFrame 使用 Mac 桌面坐标,1 point 始终是 1 个 AppKit point。如果 app 处于 Scaled to Match iPad 模式,它和 UI 元素之间会有 77% 的缩放差异。坐标原点在主显示器左上角。
6. 工具栏中的 UIView 和弹出层
(18:46)这场 session 的最后一组 API 用来精修 Mac 工具栏。
Mac Catalyst 新增能力:UIView 可以作为 item 放进 NSToolbar。如果你使用新的桌面级 iPad API,UIBarButtonItem 的 customView 会被自动包裹并加入工具栏(19:04)。
如果你自己管理 NSToolbar,可以使用新的 NSUIViewToolbarItem。它是 NSToolbarItem 的子类,初始化时接收一个 UIView(19:37)。
func toolbar(
_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
let wordCountView = WordCountView()
return NSUIViewToolbarItem(itemIdentifier: itemIdentifier, view: wordCountView)
}
关键点:
NSUIViewToolbarItem用来把UIView包进NSToolbarItem。NSToolbarDelegate的itemForItemIdentifier是创建 toolbar item 的位置。- 视频特别提醒:
NSToolbar的自定义模式需要唯一的 toolbar item instance。 - 如果你自己管理
NSToolbar,delegate 也要为每个NSUIViewToolbarItem创建唯一的UIViewinstance,不要复用同一个 view。
(20:43)这个 item 还可以作为 popover 的锚点。视频里的 Markdown app 点击字数统计 item 后,会弹出段落数、章节数、阅读时间等详情。
@objc func showWordCountDetails(_ sender: NSToolbarItem) {
let detailsController = WordCountDetailsViewController()
detailsController.modalPresentationStyle = .popover
detailsController.popoverPresentationController?.sourceItem = sender
present(detailsController, animated: true)
}
关键点:
- action 里创建 popover 的 view controller。
modalPresentationStyle = .popover使用弹出层展示。sourceItem设置为当前 toolbar item,让弹出层锚定在工具栏按钮上。- 这类模式适合 Mac 工具栏上的摘要信息和二级操作。
7. 选择是否翻译 UINavigationBar
(21:12)如果你不想让 navigation bar 自动翻译成 Mac 工具栏,可以设置 UINavigationBar 的 preferredBehavioralStyle。
navigationController.navigationBar.preferredBehavioralStyle = .pad
关键点:
- 默认值是
automatic。 - 设置为
.mac会明确请求翻译。 - 设置为
.pad会关闭 navigation bar 的自动翻译。 - 这个开关适合已经有自定义 toolbar 方案的 Catalyst app。
核心启发
-
做一个真正全屏的 iOS 游戏 Mac 版本:用
UISupportsTrueScreenSizeOnMac和UILaunchToFullScreenByDefaultOnMac处理显示,再用 Touch Alternatives 接住键盘和鼠标输入。适合先让现有触控游戏在 Mac 上可玩。 -
给文档 app 补齐 Mac 文件操作:检查 responder chain 是否覆盖 duplicate、move、rename、export functions。需要时保留 File 菜单;不需要时用
UIMenuBuilder移除.document菜单。 -
把 iPad 工具栏升级成 Mac 工具栏:采用桌面级 iPad API,让
UINavigationBar、center items 和搜索栏自动映射到NSToolbar。如果已经自己管理工具栏,用.pad关闭自动翻译。 -
为辅助信息做独立小窗口:用
UIWindowScene的 geometry request 设置初始尺寸,用windowingBehaviors和sizeRestrictions禁用不适合的窗口按钮。适合 Markdown 帮助、检查器、调色板这类辅助面板。 -
在工具栏放入状态视图:用
customView自动进入工具栏,或用NSUIViewToolbarItem包裹UIView。点击后用 popover 展示更详细内容,例如字数统计、阅读时间、同步状态。
关联 Session
- What’s new in UIKit — UIKit 的年度更新,包含 iPadOS、iOS 和 Mac Catalyst 应用会用到的桌面级 API。
- Meet desktop-class iPad — 介绍 UINavigationBar、文档菜单、搜索等桌面级 iPad 能力,这些能力会映射到 Mac Catalyst。
- Adopt desktop-class editing interactions — 讲解编辑菜单和查找交互,帮助 iPad app 在 Mac Catalyst 上获得桌面级编辑体验。
- Use SwiftUI with UIKit — 说明如何在 UIKit 应用里嵌入 SwiftUI 视图,可用于 Catalyst 工具栏和弹出内容。
评论
GitHub Issues · utterances