WWDC Quick Look 💓 By SwiftGGTeam
Bring your iOS app to the Mac

Bring your iOS app to the Mac

观看原视频

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 值是 .document11: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,UIBarButtonItemcustomView 会被自动包裹并加入工具栏(19:04)。

如果你自己管理 NSToolbar,可以使用新的 NSUIViewToolbarItem。它是 NSToolbarItem 的子类,初始化时接收一个 UIView19: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
  • NSToolbarDelegateitemForItemIdentifier 是创建 toolbar item 的位置。
  • 视频特别提醒:NSToolbar 的自定义模式需要唯一的 toolbar item instance。
  • 如果你自己管理 NSToolbar,delegate 也要为每个 NSUIViewToolbarItem 创建唯一的 UIView instance,不要复用同一个 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 工具栏,可以设置 UINavigationBarpreferredBehavioralStyle

navigationController.navigationBar.preferredBehavioralStyle = .pad

关键点:

  • 默认值是 automatic
  • 设置为 .mac 会明确请求翻译。
  • 设置为 .pad 会关闭 navigation bar 的自动翻译。
  • 这个开关适合已经有自定义 toolbar 方案的 Catalyst app。

核心启发

  • 做一个真正全屏的 iOS 游戏 Mac 版本:用 UISupportsTrueScreenSizeOnMacUILaunchToFullScreenByDefaultOnMac 处理显示,再用 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 设置初始尺寸,用 windowingBehaviorssizeRestrictions 禁用不适合的窗口按钮。适合 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