WWDC Quick Look 💓 By SwiftGGTeam
Optimize the interface of your Mac Catalyst app

Optimize the interface of your Mac Catalyst app

观看原视频

Highlight

Xcode 12 的 “Optimize Interface for Mac” 让 Mac Catalyst app 以 Mac idiom 启动,UIKit 和 SwiftUI 会采用 1:1 点尺寸、Mac 控件外观、Mac 文本尺寸和系统间距。

核心内容

把 iPad app 带到 Mac 上,第一版常常先选择 “Scale Interface to Match iPad”。这条路径兼容性好,UIKit 会尽量保持 iPad 布局。代价也清楚:界面按 77% 缩放,100 points 的代码尺寸在屏幕上变成 77 points,文本和图像都经过缩放。

“Optimize Interface for Mac” 是下一步。它是 Xcode 12 里的构建设置,app 会在启动时进入 Mac idiom。这个选择没有编译期影响,但运行时的界面度量会变:内容以 1:1 points 渲染,系统按钮、滑块、开关和活动指示器采用 Mac 外观,body 文本从 iPad 的 17 points 变成 Mac 的 13 points,Auto Layout 的 system spacing 也换成 Mac 间距。

这带来的工作量集中在布局审计。硬编码字体不会自动变成 Mac 文本尺寸;依赖控件原始大小的约束会被新控件度量影响;只提供 Universal asset 时,图像会先回退到 Mac Scaled、iPad,再回退到 Universal。Session 用 cookbook app 演示了这条迁移路径:改 cell 高度、提供 Mac asset、隐藏 navigation bar,并用 Mac idiom 的 trait variation 安装底部 Save 和 Cancel 按钮。

SwiftUI 的迁移成本更低一些。GroupBox、Toggle、Button、Picker 会根据 Mac idiom 换成 Mac 风格;自定义 ButtonStyle 不会被系统替换。Session 还展示了用 UIHostingController 把 SwiftUI view 放进 UIKit app,GroupBox、Toggle 和 Picker 在优化后自动取得 Mac 表现。

详细内容

1. 用 Mac idiom 区分优化后的 Catalyst 运行时

29:33

“Optimize Interface for Mac” 不等于新的编译目标。Session 明确说这个设置没有 compile time implications。判断优化后的运行时,应使用 traitCollection.userInterfaceIdiom == .mac;判断是否在 Mac Catalyst 环境编译,才使用 targetEnvironment(macCatalyst)

// Idiom vs conditional compilation block

if traitCollection.userInterfaceIdiom == .mac {
    // "Optimize Interface for Mac" specific code
}


#if targetEnvironment(macCatalyst)
    // Mac Catalyst specific code
#endif


if traitCollection.userInterfaceIdiom == .mac {
    // "Optimize Interface for Mac" specific code
} else if traitCollection.userInterfaceIdiom == .pad {
    #if targetEnvironment(macCatalyst)
        // Mac Catalyst specific code
    #else
        // iPad specific code
    #endif
}

关键点:

  • traitCollection.userInterfaceIdiom == .mac 对应优化后的 Mac idiom。
  • #if targetEnvironment(macCatalyst) 对应编译环境,和 idiom 不是同一层判断。
  • 最后一段把 Mac idiom、Catalyst 下的 iPad idiom、普通 iPad 三种路径拆开,适合处理 back deploy 时的差异。

2. 迁移 UIKit 布局时先处理 navigation bar 和硬编码尺寸

22:04

在 cookbook app 的 New Recipe 窗口里,iPad 风格的 navigation bar 出现在 Mac 窗口顶部。Session 的处理方式很直接:Mac idiom 下隐藏 navigation bar,再把 Save 和 Cancel 移到底部视图。它还修正了一个 17-point system font 的 label,因为动态文本样式会随平台调整,硬编码字体不会。

if traitCollection.userInterfaceIdiom == .mac {
    navigationController?.setNavigationBarHidden(true, animated: false)
}

关键点:

  • traitCollection.userInterfaceIdiom 是运行时判断,适合放在同一套 Catalyst 代码里。
  • .mac 分支只处理 Mac idiom 下的界面差异。
  • setNavigationBarHidden(true, animated: false) 对应 session 中把 navigation bar 移除的步骤。

24:47

Interface Builder 也有 Mac idiom 的 trait variation。Session 里把包含 Save 和 Cancel 的 stack view 设置为只在 Mac idiom 安装,再把原来的底部约束 priority 从 Required 降到 High。这样 iPad 仍保留原布局,Mac 则给底部按钮留出空间。

3. SwiftUI 控件会按 Mac idiom 换外观

31:26

SwiftUI 的 GroupBox 在 optimized for Mac 后会呈现 Mac GroupBox。Session 用嵌套 GroupBox 展示结构化内容,系统会给不同层级分配背景和 padding。

// Nested GroupBoxes

struct ContentView: View {
    var body: some View {
        GroupBox {
            VStack {
                Text("High level information")
                GroupBox {
                    Text("Some elaborate details")
                }
            }
        }
    }
}

关键点:

  • 外层 GroupBox 表示一组结构化内容。
  • VStack 组织组内垂直内容。
  • 内层 GroupBox 展示更深一层的信息,optimized for Mac 后会使用 Mac GroupBox 的视觉层级。

32:00

Toggle 的默认外观也会变化。iPadOS 上默认是 sliding switch,Mac idiom 下默认是 checkbox。Session 还说明,若默认样式不合适,可以使用 ToggleStyle structs 和 ToggleStyle modifier 指定样式。

// DefaultToggleStyle

struct ContentView: View {
    @State var completed: Bool = false

    var body: some View {
        Toggle("Complete?", isOn: $completed)
    }
}

关键点:

  • @State var completed 保存 Toggle 的布尔状态。
  • Toggle("Complete?", isOn: $completed) 使用默认 ToggleStyle。
  • 默认样式会根据 Mac idiom 显示为 checkbox。

33:14

Picker 在 Mac idiom 下会使用 Mac picker。Session 里的示例用整数 tag 绑定 Small、Medium、Large 三个选项。

// DefaultPickerStyle

struct ContentView: View {
    @State var sizeIndex = 2

    var body: some View {
        Picker("Size:", selection: $sizeIndex) {
            Text("Small").tag(1)
            Text("Medium").tag(2)
            Text("Large").tag(3)
        }
    }
}

关键点:

  • @State var sizeIndex = 2 让 Medium 成为初始选择。
  • Picker("Size:", selection: $sizeIndex) 把控件绑定到状态。
  • 每个 Text(...).tag(...) 提供一个可选值,Mac idiom 下会呈现 Mac 用户熟悉的选择控件。

4. 自定义按钮样式会保留

33:55

系统按钮会采用 Mac 外观。自定义 ButtonStyle 不会被 Catalyst 改写。Session 用一个自定义渐变按钮说明,SwiftUI 和 Catalyst 会让自定义 body 原样保留。

// Custom gradient button

struct CustomNinetiesButtonStyle: ButtonStyle {
    var angle: Angle = .degrees(54.95)

    func gradient(shifted: Bool) -> AngularGradient {
        let lightTeal = Color(#colorLiteral(red: 0.2785285413, green: 0.9299042821, blue: 0.9448828101, alpha: 1))
        let yellow    = Color(#colorLiteral(red: 0.9300076365, green: 0.8226149678, blue: 0.59575665, alpha: 1))
        let pink      = Color(#colorLiteral(red: 0.9437599778, green: 0.3392140865, blue: 0.8994731307, alpha: 1))
        let purple    = Color(#colorLiteral(red: 0.5234025717, green: 0.3247769475, blue: 0.9921132922, alpha: 1))
        let softBlue  = Color(#colorLiteral(red: 0.137432307, green: 0.5998355746, blue: 0.9898411632, alpha: 1))

        let gradient = Gradient(stops:
                                    [.init(color:lightTeal, location: 0.2),
                                     .init(color: softBlue, location: 0.4),
                                     .init(color: purple, location: 0.6),
                                     .init(color: pink, location: 0.8),
                                     .init(color: yellow, location: 1.0)])

        return AngularGradient(gradient: gradient, center: .init(x: 0.25, y: 0.55), angle: shifted ? angle : .zero)
    }

    func makeBody(configuration: ButtonStyleConfiguration) -> some View {
        let background = NinetiesBackground(isPressed: configuration.isPressed,
                                            pressedGradient: gradient(shifted: false),
                                            unpressedGradient: gradient(shifted: true))
        return configuration.label
            .foregroundColor(configuration.isPressed ? Color.pink : Color.white)
            .modifier(background)
    }

    struct NinetiesBackground: ViewModifier {
        let isPressed: Bool
        let pressedGradient: AngularGradient
        let unpressedGradient: AngularGradient

        func body(content: Content) -> some View {
            let foreground = content
                    .padding(.horizontal, 24)
                    .padding(.vertical, 14)
                    .foregroundColor(.white)
            return foreground
                .background(Capsule().fill(isPressed ? pressedGradient : unpressedGradient))
        }
    }
}
struct ContentView: View {

    var body: some View {
      Button("Awesome", action: {})
          .buttonStyle(CustomNinetiesButtonStyle())
    }
}

关键点:

  • CustomNinetiesButtonStyle 实现 ButtonStyle,样式逻辑由开发者控制。
  • configuration.isPressed 根据按下状态切换颜色和背景。
  • .buttonStyle(CustomNinetiesButtonStyle()) 明确应用自定义样式,Catalyst 不会把它替换成系统 Mac button。

核心启发

1. 做一个 Catalyst 布局审计面板

  • 做什么:在 app 内部列出 Mac idiom 下需要检查的视图:硬编码字体、固定尺寸图片、依赖控件原始尺寸的约束、system spacing 变化。
  • 为什么值得做:Session 显示 optimized for Mac 会改用 1:1 points、Mac 控件度量和 Mac 文本尺寸,简单视图也可能出现换行、图片变大、slider 变窄。
  • 怎么开始:先在 Mac idiom 分支里标记关键 view controller,再逐个检查 traitCollection.userInterfaceIdiom == .mac 下的布局差异。

2. 给常用图片补齐 Mac asset

  • 做什么:把列表图标、favorite 图标、地图标注这类高频图像补上 Mac 版本。
  • 为什么值得做:Session 里说明,optimized for Mac 会优先使用 Mac asset;找不到时会回退到 Mac Scaled、iPad、Universal,尺寸可能不合适。
  • 怎么开始:在 Asset Catalog 的 Devices inspector 勾选 Mac,先从列表 cell 的小图标开始替换。

3. 把 iPad 导航动作改成 Mac 窗口动作

  • 做什么:把新建、保存、取消这类窗口级动作从 iPad navigation bar 移到 Mac 更合适的位置。
  • 为什么值得做:Session 中 New Recipe 窗口隐藏 navigation bar,并把 Save 和 Cancel 放到底部,因为这些动作直接作用于当前窗口里的表单数据。
  • 怎么开始:在 Mac idiom 下隐藏 navigation bar;用 Interface Builder 的 Mac trait variation 安装专用按钮区。

4. 用 SwiftUI 替换静态说明视图

  • 做什么:把纯文本说明页改成 SwiftUI 交互视图,例如 GroupBox 分组、Toggle 勾选步骤、Picker 切换单位。
  • 为什么值得做:Session 展示了把 SwiftUI view 通过 UIHostingController 放进 UIKit app,GroupBox、Toggle、Picker 会自动获得 Mac 外观。
  • 怎么开始:先做一个局部 ContentView,复用 session 中的 GroupBox、Toggle、Picker 模式,再嵌入现有 UIKit 页面。

关联 Session

评论

GitHub Issues · utterances