Use SwiftUI with AppKit
Swift & UI 进阶 20m

在 AppKit 项目中使用 SwiftUI

Use SwiftUI with AppKit

2022年6月6日

在 Apple 官方观看视频

一句话判断

如果你的 Mac App 已经有大量 AppKit 代码,这场 Session 以 Shortcuts 为案例,详细讲解了 SwiftUI 和 AppKit 互相嵌入、数据共享、响应链集成等实战技巧。

这场 Session 讲了什么

Shortcuts 团队的 Ian 以 Shortcuts App 为实际案例,系统讲解了在 macOS App 中混合使用 SwiftUI 和 AppKit 的各种场景。

内容覆盖六个方面:用 NSHostingController 在 AppKit 中嵌入 SwiftUI View、通过 ObservableObject 在 AppKit 和 SwiftUI 之间共享状态、在 NSCollectionView/NSTableView 的 Cell 中高效托管 SwiftUI View、SwiftUI View 的布局和尺寸与 AppKit Auto Layout 的配合、让 SwiftUI View 参与 AppKit 响应链和焦点系统、以及在 SwiftUI 中嵌入 AppKit View(NSViewRepresentable)。

值得深挖的点

CollectionView Cell 中的 SwiftUI 托管技巧。 关键在于避免每次 Cell 重用时都创建新的 HostingView。正确做法是保持一个 NSHostingView 实例,通过修改 rootView 来更新内容。这样 SwiftUI 可以复用已有的 View 层级(如 VStack、Spacer),只更新变化的部分(图片、文字、背景)。

响应链的桥接。 SwiftUI View 默认不参与 AppKit 的响应链。如果你的 SwiftUI 编辑器需要处理菜单栏命令(如 Cut/Copy/Paste 或自定义操作),需要让 SwiftUI View 正确接入 AppKit 的 first responder 机制。这在混合架构中是一个容易被忽略的集成点。

macOS Ventura 新增的约束控制 API。 NSHostingView 和 NSHostingController 现在可以精细控制自动创建的约束类型(最小尺寸、理想尺寸、最大尺寸),对于性能敏感或已有复杂约束体系的场景很有帮助。

代码片段

// 在 AppKit Split View 中托管 SwiftUI 侧边栏
let sidebarView = SidebarView(selectedItem: $selectedItem)
let hostingController = NSHostingController(rootView: sidebarView)
let splitViewItem = NSSplitViewItem(sidebarWithViewController: hostingController)
splitViewController.insertSplitViewItem(splitViewItem, at: 0)
// 通过 ObservableObject 共享状态
class SelectionModel: ObservableObject {
    @Published var selectedItem: SidebarItem?
}

// AppKit 端可以读取 model 的状态来切换详情页
// SwiftUI 端通过 @EnvironmentObject 自动响应变化
// CollectionView Cell 中高效托管 SwiftUI
class ShortcutCell: NSCollectionViewItem {
    private var hostingView: NSHostingView<ShortcutView>?

    func displayShortcut(_ shortcut: Shortcut) {
        let view = ShortcutView(shortcut: shortcut)
        if let hostingView = hostingView {
            // 复用已有 HostingView,只更新 rootView
            hostingView.rootView = view
        } else {
            // 首次创建
            let newHosting = NSHostingView(rootView: view)
            view.addSubview(newHosting)
            hostingView = newHosting
        }
    }
}

最佳实践

  • Cell 托管时复用 HostingView。 不要在每次 Cell 配置时创建新的 HostingView,复用同一个实例并通过修改 rootView 更新内容。
  • SwiftUI View 的布局由 Auto Layout 驱动。 HostingView 会自动创建 intrinsic size、min/max size 的约束,你在 AppKit 端只需添加到父视图的约束即可。
  • Modal 展示用 NSHostingController 的 present 方法。 presentAsSheetpresentAsModalWindow、Popover API 都可以直接用 HostingController,窗口大小自动适配内容。
  • macOS Ventura 用户关注新的约束控制 API。 如果默认的自动约束导致性能问题或不必要的布局计算,可以按需禁用。

还有什么值得关注

  • SwiftUI View 作为窗口 contentView 时,会自动更新窗口的 min/max size
  • 推荐观看 WWDC22 的 “What’s new in AppKit” 了解 macOS Ventura 的其他 AppKit 更新
  • Shortcuts 的案例说明跨平台 App(iOS/watchOS/macOS)可以共享大量 SwiftUI 代码,macOS 特有的部分用 AppKit 补充
  • NSViewRepresentable 用于反向嵌入——在 SwiftUI 中使用已有的 AppKit View
WWDC 2022