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
- What’s new in Mac Catalyst — 补充 Mac Catalyst 在 2020 年的整体更新,并提到 optimized for Mac mode 的上层背景。
- Accessibility design for Mac Catalyst — 继续处理 Mac Catalyst app 在键盘、鼠标和无障碍导航上的体验。
- Handle trackpad and mouse input — 讲 indirect input、pointer movement、scroll input 和 gesture recognizer 的处理方式。
- Adopt the new look of macOS — 从 AppKit 和 macOS Big Sur 设计角度理解 Mac 控件、工具栏和视觉层级。
评论
GitHub Issues · utterances