在 Mac 应用中显示一小行标签时,使用 ViewThatFits 选择能够放下的最长完整前缀,并将隐藏项目数量计入其中。本教程会构建这样一行内容,不截断标签,也不在布局期间更新状态。
具体任务是在可调整大小的 macOS 窗口中显示六个合成标签。保持它们的顺序,只显示完整标签,并为省略的后缀显示诸如 还剩 3 项 这样的徽标。此方案假设水平空间足以容纳仅显示数量的徽标,垂直空间足以容纳一行内容。
选择完整的行候选项
Apple 将 ViewThatFits 说明为:在受约束的轴上,选择第一个理想尺寸适合所提议尺寸的子视图。macOS 13 及更高版本支持它。此处使用 .horizontal,因为任务关注的是行宽;外围界面必须提供行所需的高度。ViewThatFits
对于六个标签,按以下顺序提供七个候选项:六个标签且不带徽标、五个标签加 还剩 1 项、四个标签加 还剩 2 项,依此类推,最后只显示 还剩 6 项。每个候选项都是一行完整内容。徽标也参与适配判断,因此添加数量不会意外挤掉最后一个可见标签。
这实现了前缀策略:如果第一个标签很长,即使后面的短标签能够放下,也可能导致所有标签都被省略。这是有意为之。如果你的任务允许跳过单个标签,请先定义不同的选择策略,再进行实现。
对每个候选行应用 fixedSize(),使其标签和徽标保持理想尺寸。Apple 说明,此修饰符可以让视图超出父视图的边界;它本身并不保证视图一定适合。此处由外层选择容器执行适配判断。fixedSize()
构建完整的 macOS 示例
在 Xcode 中创建一个 macOS SwiftUI 应用,并将部署目标设为 macOS 13 或更高版本。用以下单文件程序替换自动生成的应用声明和内容视图。确保目标中恰好有一个标记为 @main 的应用声明。
示例使用显式标识符,而不是用标签文本作为身份标识。Apple 的 ForEach 文档要求元素具有标识,或显式指定标识符键路径;不同的标识符也允许两个项目显示相同的标题。ForEach
import SwiftUI
struct DemoItem: Identifiable {
let id: Int
let title: String
}
struct ItemChip: View {
let title: String
var body: some View {
Text(title)
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.12))
)
}
}
struct PrefixCandidate: View {
let items: [DemoItem]
let visibleCount: Int
var body: some View {
HStack(spacing: 8) {
ForEach(items.prefix(visibleCount)) { item in
ItemChip(title: item.title)
}
if visibleCount < items.count {
Text("\(items.count - visibleCount) more")
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.15))
)
}
}
.fixedSize()
}
}
struct WholeItemRow: View {
let items: [DemoItem]
var body: some View {
if items.isEmpty {
Text("No items")
.fixedSize()
} else {
ViewThatFits(in: .horizontal) {
ForEach(
Array((0...items.count).reversed()),
id: \.self
) { visibleCount in
PrefixCandidate(
items: items,
visibleCount: visibleCount
)
}
}
}
}
}
struct ContentView: View {
let items: [DemoItem] = [
DemoItem(id: 1, title: "Sketch"),
DemoItem(id: 2, title: "Color samples"),
DemoItem(id: 3, title: "Shapes"),
DemoItem(id: 4, title: "Paper textures"),
DemoItem(id: 5, title: "Layout notes"),
DemoItem(id: 6, title: "Preview")
]
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Sample labels")
.font(.headline)
WholeItemRow(items: items)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Resize the window to change how many labels fit.")
.foregroundStyle(.secondary)
}
.font(.system(size: 14))
.padding(24)
.frame(minWidth: 320, idealWidth: 680, minHeight: 160)
}
}
@main
struct WholeItemsDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
候选项标识符表示可能显示的项目数量,从项目总数递减到零。在每个候选项中,项目标识符用于标识各个标签。每次迭代都会创建一个 PrefixCandidate,其中包含一行水平排列的内容。
外层的弹性框架让行接收可用的水平空间,同时使选定内容与前导边缘对齐。将 fixedSize() 保留在 ViewThatFits 内部的候选项上。若将其应用于整个选择容器,就会改变该容器对父视图宽度提议的响应方式。
这是一个完整的源代码示例,但此处未能编译或运行,因为没有可用的 Swift 工具链。下一节中的示例计算已单独核对;实际文本宽度仍需在 Mac 应用中验证。
计算适配宽度时将徽标纳入其中
实用的判断规则是:选择整个候选项都能放下的最大可见项目数量。隐藏数量始终等于项目总数减去该候选项中可见项目的数量。无需单独估算并同步数量。
考虑一行包含四个项目的示例,其宽度分别为 72、96、64 和 88 点,包含项目内边距。假设间距为 8 点,任何非零隐藏数量的徽标宽度均为 60 点。这些是虚构的计算输入,并非对示例应用的测量结果。
当四个项目全部可见时,所需宽度为 72 + 96 + 64 + 88 + 3 × 8 = 344 点。当三个项目可见时,徽标会取代被省略的后缀,因此宽度变为 72 + 96 + 64 + 3 × 8 + 60 = 316 点。候选项包含三个项目和一个徽标,因此需要三个间距。
| 可用宽度 | 可见项目 | 隐藏项目 | 选定候选项宽度 |
|---|---|---|---|
| 344 点 | 4 | 0 | 344 点 |
| 343 点 | 3 | 1 | 316 点 |
| 300 点 | 2 | 2 | 244 点 |
| 220 点 | 1 | 3 | 140 点 |
| 60 点 | 0 | 4 | 60 点 |
在程序中,每个候选项都包含实际的徽标文本。因此,即使徽标字符串的宽度不同,也无需依赖示例中固定宽度的假设。候选项宽度不必按固定数值递减;可见项目数量递减表达了选择偏好。
仅显示数量的候选项确定了最小可用宽度。如果连这个徽标都无法放下,那么按照这些规则,就不可能在可用宽度内显示完整数量。请提供足够的最小宽度、允许采用其他布局,或将数量移到别处。不要把裁切视为成功显示完整项目。
示例为内容宽度设置了最小值,并为简短的合成徽标指定了适中的字体。如果更改文案、字体、内边距或外围布局,请再次验证仅显示数量的情况。为此示例选定的最小宽度并非通用的徽标尺寸。
将布局选择与视图生命周期分开
隐藏数量属于每个候选项的不可变输入。程序不会在测量方法中写入绑定、安排异步数量更新,也不会根据外观回调推断哪些项目可见。选择某一行时,也会同时选择与其匹配的徽标。
保持重复的候选内容简单。在多个候选项中重复一个标签,并不意味着该标签在所有候选项中共用同一身份。Apple 说明,状态存储与视图身份相关联,身份变化可能会替换状态。Demystify SwiftUI
如果之后将标签替换为可编辑控件,请将它们共享的值保存在共同祖先视图或模型中,并向各个呈现视图传入相应输入或绑定。Apple 建议将共享状态放在需要它的视图的最近共同祖先中。不要依赖候选项本地状态在呈现方式改变后仍然保留。Managing user interface state
此方案会在选定行中显示隐藏数量,但不会将选定数量暴露给同级视图或应用模型。Apple 文档中的 ViewThatFits 初始化方法接受受约束的轴和候选内容,但不提供选定数量绑定。请将导出该值视为一项额外的设计要求。init(in:content:)
了解自定义布局何时需要不同的设计
当你需要专门的几何布局时,自定义 Layout 才合适。Apple 将其核心职责定义为报告组合尺寸,并通过布局代理指定子视图位置。这些代理提供测量和放置操作,而不能直接访问底层视图。Layout
不要仅仅在放下最后一个项目后停止放置,就实现省略行为。Apple 的 LayoutSubview 文档要求每个子视图都放置一次。跳过放置不是文档所述的隐藏机制,而且布局代理不提供通用的内容重写操作,无法将另一个子视图转换成新的数量标签。LayoutSubview
如果你构建自定义布局,请确保测量和放置保持一致,包括间距和使用的尺寸提议。Apple 明确要求这些计算保持一致。即使布局会探测多种可能的排列方式,这项约定仍然适用。placeSubviews(in:proposal:subviews:cache:)
这种候选项方案的适用范围有意设定得较小。对于六个项目,它会描述七行候选项,并在这些行中共出现 21 次项目。在一般情况下,项目出现次数按 n × (n + 1) / 2 增长。这只是结构上的数量统计,并非性能基准,也不表示每次更新都会渲染所有候选项。此方案适用于少量标签;若要扩展到大量项目或内容开销较大的情况,请先进行性能分析。
在边界情况下验证行布局
运行应用并缓慢调整窗口大小。在改造组件前,检查以下情况:
- 窗口较宽时,所有标签都会显示,且不会出现
还剩 0 项的徽标。 - 窗口变窄时,标签会从末尾开始消失,不会出现部分标签或省略号。
- 可见项目数量加上徽标中的隐藏数量,始终等于六。
- 如果第一个标签特别长,仅显示数量的候选项仍然可用。
- 数组为空时,该行显示
没有项目。 - 标题重复但标识符不同的情况下,两个项目仍会计入数量。
- 更改字体或徽标文案后,仅显示数量的候选项在支持的最小宽度下仍然适配。
对于一行数量较少且顺序固定的内容,完整候选项让适配规则一目了然:每个候选项都包含其标签、间距和准确的隐藏数量。明确设置最小宽度条件并让重复内容保持轻量后,这一行就能适应窗口大小变化,而不会出现不完整项目或单独的数量更新周期。
来源
- ViewThatFits — Apple
- fixedSize() — Apple
- ForEach — Apple
- Demystify SwiftUI — Apple
- Managing user interface state — Apple
- init(in:content:) — Apple
- Layout — Apple
- LayoutSubview — Apple
- placeSubviews(in:proposal:subviews:cache:) — Apple