
把一个 iOS App 放到 Mac 上运行,拖动窗口边缘,变的只有宽高吗?
在 macOS 27 上做了一次小实验,答案颇有意味:随着用户调整窗口大小,SwiftUI 环境中的 Size Class 也会发生变化。原本常在横竖屏适配中见到的 compact 和 regular,如今在鼠标拖动之间,有了更直观的表现。

这件事值得留意。对于沿用固定设备假设的界面,一次再普通不过的窗口缩放,就可能让布局捉襟见肘。
说起 Size Class,许多开发者会条件反射地想到"iPhone 用 compact,iPad 用 regular"。这套经验便于入门,却容易先入为主。
SwiftUI 通过两个环境值描述水平和垂直方向的空间分类:
swift
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
两者的取值可以是 .compact、.regular,也可能为 nil。水平对应宽度,垂直对应高度。它们描述的是视图所处的布局环境,不能简单当成设备型号的别名。相关定义可见 Apple 的 UserInterfaceSizeClass 文档。
因此,看见一个宽窗口,便断言两个方向都是 regular,难免以偏概全。窗口的宽与高是两回事,两个方向的分类也应分别观察。

这次实验的做法很直接:让界面同时显示实际可用尺寸与两个 Size Class,然后拖动窗口,观察数值。核心代码并不复杂:
swift
struct SizeClassProbe: View {
@Environment(\.horizontalSizeClass) private var horizontal
@Environment(\.verticalSizeClass) private var vertical
var body: some View {
GeometryReader { proxy in
VStack(alignment: .leading, spacing: 12) {
Text("宽:\(proxy.size.width, specifier: "%.1f") pt")
Text("高:\(proxy.size.height, specifier: "%.1f") pt")
Text("水平:\(name(horizontal))")
Text("垂直:\(name(vertical))")
}
.padding()
}
}
private func name(_ value: UserInterfaceSizeClass?) -> String {
switch value {
case .compact: "compact"
case .regular: "regular"
case nil: "nil"
@unknown default: "unknown"
}
}
}
这里有个细节不能张冠李戴:GeometryReader 测量的是当前视图获得的布局空间,单位为点;它既不是显示器分辨率,也不是包含标题栏的窗口外框尺寸。将它与 Size Class 并排展示,恰好可以对照"具体尺寸"和"空间分类"这两个层次。

测试入口同样要选对。在 Xcode 中,应选择 My Mac (Designed for iPad) ,运行 iOS App 的 Mac 版本。多平台项目里的普通 My Mac 则可能启动原生 macOS App。两者虽然都出现在 Mac 桌面上,观察结果却不能混为一谈。
在本次 macOS 27 实测中,拖动 iOS App 窗口,确实能够观察到 Size Class 随之变化。这个结果说明:至少在当前测试环境下,分类会随着窗口调整而更新,不能将启动时读到的值视作一成不变。
不过,窗口尺寸连续变化,Size Class 只有少量离散状态。拖动了一段距离,数值变了,分类保持原样,完全可能。此次观察也不足以归纳出适用于所有设备、系统版本和运行方式的固定阈值。把某次测得的宽度刻进代码,反倒容易刻舟求剑。

对 SwiftUI 开发者而言,接下来便顺理成章:让布局持续读取环境,在分类变化时重新组织内容。例如,在水平空间宽裕时并排展示两块内容,在紧凑时改为上下排列:
swift
struct AdaptiveContent: View {
@Environment(\.horizontalSizeClass) private var horizontal
private var layout: AnyLayout {
horizontal == .regular
? AnyLayout(HStackLayout(alignment: .top, spacing: 20))
: AnyLayout(VStackLayout(alignment: .leading, spacing: 20))
}
var body: some View {
layout {
GroupBox("概览") {
Text("主要信息")
}
GroupBox("详情") {
Text("补充内容")
}
}
.padding()
}
}
代码不必监听鼠标拖动,也不必把 Size Class 复制到一份额外的状态中。环境值更新后,SwiftUI 会重新求值,布局自然跟着调整。这里将 nil 一并落入纵向布局,是示例明确选择的回退策略。
当然,Size Class 也不能包办所有适配。两块内容是否真的放得下,还受文字长度、字体大小和容器空间影响。决定界面的整体组织方式,可以参考 Size Class;判断一组按钮能否并排容纳,则更适合交给 ViewThatFits 或具体的布局计算。各司其职,才能游刃有余。

macOS 27 上的这次实验,把一个老道理摆到了眼前:用户手中的窗口,随时都可能改变界面的空间条件。
我们写下的是布局规则,用户决定的是窗口大小。让界面懂得因地制宜,拖动窗口时的从容,便会水到渠成。
感谢宝子们的观赏,我们下次再会吧!8-)