要修复 SwiftUI 内容在 Mac 窗口调整大小时漂移至屏幕外的问题,首先需要确定是绘制超出了布局边界、内容拒绝收缩,还是嵌入的 AppKit 视图停止跟踪其容器。然后修复该特定边界。
本指南面向正在修复自家 Mac 应用可调整大小界面的开发人员:保持标签可读、控件可达、较长内容可滚动。以下诊断序列使用合成标签,并区分 SwiftUI 布局、窗口尺寸调整和 AppKit 文本换行。
识别调整大小时哪个边界失效
SwiftUI 布局始于父视图的尺寸提议。子视图根据提议选择自身尺寸;提议并非要求将所有内容裁剪至该矩形。Apple 在 ProposedViewSize 中记录了最小、最大和理想尺寸提议。这解释了为什么变窄窗口不一定会使每个后代视图变窄。
从可重复的测试开始:打开应用窗口,逐渐变窄直到首次出现故障,然后再变宽。观察首个受影响的元素,而非最终排列。标签是否延伸至容器之外?整个面板是否位移?文本是否保持之前的换行宽度?
在可疑子视图上临时添加 .border(Color.orange),在其外层 frame 或容器上添加 .border(Color.blue)。将修饰符放在不同层级以便比较其矩形。Apple 的 offset 文档 演示了这种边框技术,用于区分原始布局位置与位移后的绘制。
| 观察到的现象 | 首先检查 | 有用的后续操作 |
|---|---|---|
| 绘制移出原本稳定的矩形 | offset、position、坐标计算 |
逐个移除定位修饰符 |
| 容器变窄时标签保持宽宽 | 水平 fixedSize、显式宽度 |
让标签接受受约束的宽度 |
| 控件无法在水平行中容纳 | 组合控件宽度和间距 | 提供垂直替代方案 |
| 高度减小时下部内容消失 | 缺少溢出行为 | 使内容区域可滚动 |
| 托管的 SwiftUI 面板在 AppKit 中位移 | 宿主视图边界和尺寸约束 | 对比宿主尺寸与内容需求 |
| AppKit 文本视图变窄但行宽不变 | 文本容器宽度跟踪 | 检查文本视图和文本容器配置 |
这些是基于文档行为得出的诊断假设,并不保证特定症状只有一个原因。一次更改一个可疑项并重复相同的调整大小手势。如果用 Text("Sample panel") 替换复杂子视图后故障消失,则逐步恢复该子视图的各层。
移除抗拒窗口变化的定位和尺寸规则
检查偏移量和基于中心的定位
offset 会在不改变视图原始尺寸的情况下平移显示内容。因此,正向水平偏移量即使父视图仍保留原始空间,也可能将绘制推至右边缘之外。暂时移除偏移量;如果绘制返回容器内部,则用栈、内边距或 frame 对齐来表达预期的排列。请参阅 Apple 的 offset 行为。
position(x:y:) 将视图的中心放置在其父视图坐标空间中的坐标处。将这些坐标视为左上角会引入与子视图尺寸相关的错误。一次性计算的位置在父视图尺寸改变时也可能变得不合适。对于普通的标签和控件面板,首选能表达与容器预期关系的对齐方式。Apple 在 position(x:y:) 中定义了基于中心的行为。
例如,.frame(maxWidth: .infinity, alignment: .leading) 表示在灵活 frame 内的前沿对齐。它并不会移除超大后代视图自身的尺寸要求。Apple 的 灵活 frame 文档 解释了 frame 如何约束提议并定位其子视图。
检查固定宽度和理想尺寸文本
检查故障分支上的显式宽度设置和水平使用的 fixedSize。Apple 明确记录了 .fixedSize(horizontal: true, vertical: false) 会使文本保留整个字符串所需的理想宽度,可能超出父视图边界。对于换行的正文文本,移除该水平要求。在需要保留换行文本完整高度的场合,可考虑 .fixedSize(horizontal: false, vertical: true)。请参阅 fixedSize(horizontal:vertical:)。
保留垂直尺寸需要有地方容纳该高度。短小且不可滚动的容器中垂直增长的段落仍可能变得不可达。当任务需要访问所有文本时,使用垂直滚动区域;Apple 在 ScrollView 中记录了支持的滚动轴。
重新检查基于几何的计算
GeometryReader 使用自身的尺寸和坐标空间定义内容,并向父视图返回灵活的首选尺寸。在视图周围添加它会改变布局结构,因此它不是中立的测量包装器。如果在引入几何计算后开始出现漂移,暂时用普通栈和 frame 替换该分支。Apple 在 GeometryReader 中描述了这些属性。
对于真正需要几何信息的比例布局,从相关的当前容器推导尺寸。检查任何在初次出现时捕获宽度的状态。还要检查测量的尺寸是否用于调整被测量区域自身的大小;移除该依赖是一个有用的隔离测试。
构建具有显式溢出行为的紧凑布局
以下完整示例演示了三个独立选择:文本换行、控件在需要时切换排列、垂直溢出保持可达。其尺寸为说明性设计输入,非 Apple 规定值。
在 Xcode 中创建 macOS SwiftUI 应用,将部署目标设为 macOS 13 或更高版本,并用此单文件替换生成的应用和内容视图声明。仅保留一个 @main 声明。示例使用 macOS 13 可用的 ViewThatFits 和窗口可调整大小 API。Apple 在 ViewThatFits 和 windowResizability(_:) 中记录了它们的行为。
import SwiftUI
@main
struct ResizeExampleApp: App {
var body: some Scene {
WindowGroup {
ResizeExampleView()
.frame(minWidth: 280, minHeight: 220)
}
.windowResizability(.contentMinSize)
}
}
struct ResizeExampleView: View {
@State private var selection = "Sample A"
private let details = Array(
repeating: "Sample notes wrap as the panel becomes narrower.",
count: 12
).joined(separator: " ")
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Sample panel")
.font(.title2)
Text(details)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
ViewThatFits(in: .horizontal) {
HStack(spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
.fixedSize(horizontal: true, vertical: false)
VStack(alignment: .leading, spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
}
Text("Selected: \(selection)")
}
.padding(20)
.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .leading
)
}
}
}
ViewThatFits 按声明顺序评估备选方案,并选择第一个在受约束轴上理想尺寸适配的方案。此处水平适配在按钮行和垂直栈之间选择。行刻意保留其水平理想尺寸,以免压缩的行成为首选结果。保持两个备选方案在可用操作上等价。请参阅 Apple 的 选择规则。
外层 frame 允许内容列跟随可用宽度,而垂直滚动视图在换行增加高度时提供访问。此排列假设单个控件在选定的最小宽度下能容纳。较长标签或较大字体可能需要不同的最小宽度或不同的紧凑排列。
此示例经检查声明一致且 API 可用;因无 Swift 编译器未在此编译。在 macOS 上运行以验证实际窗口和文本行为。
在修复内容布局后设置窗口限制
有用的最小窗口尺寸可防止界面变小到不低于其支持的排列。使用 .contentMinSize,SwiftUI 从内容推导窗口最小尺寸,且不设最大尺寸。示例中根 frame 提供最小宽度和高度输入。请参阅 contentMinSize。
通过使用最长支持的标签测试紧凑排列来选择该最小值。增加最小值直到错位视图可见可能掩盖原始布局错误。先移除非预期位移和超大子视图;再设定预期任务仍可用的最小尺寸。
如果 AppKit 拥有窗口并托管 SwiftUI,单独检查托管边界。在 macOS 13 及更高版本,NSHostingView.sizingOptions 控制从 SwiftUI 内容推导的约束。当宿主是窗口的 contentView 时,它也会更新窗口的内容最小和最大尺寸。Apple 进一步记录了当宿主 frame 与其 SwiftUI 内容所需尺寸不同时,内容会居中。在添加补偿偏移量前检查这些关系。请参阅 NSHostingView sizingOptions。
仅当故障视图使用 AppKit 和 TextKit 时才检查它们
让 SwiftUI 为代表的根视图定尺寸
对于 NSViewRepresentable,SwiftUI 控制所代表 AppKit 视图的 frame 和 bounds。Apple 明确警告自行设置这些属性会与 SwiftUI 冲突并产生未定义行为。如果你的调整大小处理程序反复赋值代表根视图的 frame,请移除这种竞争所有权,通过 SwiftUI 边界表达尺寸。请参阅 NSViewRepresentable。
当需要自定义测量时,macOS 13 及更高版本提供 sizeThatFits(_:nsView:context:)。它可在一次布局过程中接收多个提议;返回 nil 请求默认尺寸。使结果依赖于当前提议和内容,而非之前的窗口宽度。未指定的提议维度需要有意的测量策略,而非自动视为零。请参阅 representable sizing。
Swift 的可选绑定提供了一种在使用前检查并提取提议维度的方式。这在实现尺寸策略时避免强制解包可选宽度或高度很有用。语言行为记录在 The Basics: Optional Binding 中。
将文本换行宽度与文本视图宽度分离
如果故障子视图是 NSTextView,对比其当前宽度与其文本容器的宽度。Apple 记录 NSTextContainer.widthTracksTextView 默认为 false;将其设为 true 可使容器在文本视图宽度变化时调整其宽度。这是行应随文本视图回流时的相关控制。请参阅 widthTracksTextView。
还要检查 isHorizontallyResizable。它控制文本视图是否改变宽度以适应其文本,这与在指定宽度内换行文本是不同的需求。对于垂直滚动、换行的文本区域,统一配置这两个策略,并确保包含布局确实调整文本视图尺寸。请参阅 isHorizontallyResizable。
保留实现使用的文本引擎。Apple 记录在 NSTextView 或其文本容器上访问 layoutManager 会将其切换为使用 NSLayoutManager 的兼容模式。在诊断 TextKit 2 视图时不要随意添加该访问。对现有引擎和部署目标使用适当的 API。请参阅 NSTextView。
跨宽度和高度变化验证修复
在你的应用中使用此最终序列:
- 缓慢收窄窗口通过原本故障的点,再变宽。
- 独立减小高度并确认所有必需内容仍可达。
- 用长合成标签和多行文本重复。
- 在宽松和紧凑排列下激活每个控件。
- 对 AppKit 文本视图,确认换行在两个调整方向上都跟随当前宽度。
成功的修复将定位绑定到当前容器,给内容提供可行的紧凑排列,并提供对溢出的访问。一旦这些行为在重复调整大小时保持,移除临时边框并保留由界面证明合理的最小窗口尺寸。
来源
- ProposedViewSize — Apple
- offset(x:y:) — Apple
- position(x:y:) — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- ScrollView — Apple
- GeometryReader — Apple
- ViewThatFits — Apple
- windowResizability(_:) — Apple
- contentMinSize — Apple
- sizingOptions — Apple
- NSViewRepresentable — Apple
- sizeThatFits(_:nsView:context:) — Apple
- The Basics — Swift.org
- widthTracksTextView — Apple
- isHorizontallyResizable — Apple
- NSTextView — Apple