构建流式布局
动态地将项目换到多行显示。
构建流式布局 是 CoddyKit 上的免费 Swift Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Swift Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Swift Academy 课程共包含 4 节课。
什么是流式布局
流式布局从左到右放置项目;当下一个项目会超出可用宽度时,就换到新的一行,类似标签小块或文字换行。
计划
我们将在共享辅助方法中计算换行位置,从 sizeThatFits 返回总高度,并在 placeSubviews 中重新使用相同的计算。共享逻辑可以让这两个方法保持一致。
建模放置信息
一个小型结构体记录每个子视图的放置位置。预先计算所有放置信息,可以让两个协议方法都变得简单。
struct Placement {
let index: Int
let point: CGPoint
let size: CGSize
}换行算法
遍历子视图并跟踪当前的 x。如果某个子视图会超出最大宽度,就重置 x,并按照行高加间距的距离向下移动。
func layout(_ subviews: Subviews, maxWidth: CGFloat) -> (placements: [Placement], height: CGFloat) {
var placements: [Placement] = []
var x: CGFloat = 0, y: CGFloat = 0, rowHeight: CGFloat = 0
let spacing: CGFloat = 8
for (i, s) in subviews.enumerated() {
let size = s.sizeThatFits(.unspecified)
if x + size.width > maxWidth, x > 0 {
x = 0; y += rowHeight + spacing; rowHeight = 0
}
placements.append(Placement(index: i, point: CGPoint(x: x, y: y), size: size))
x += size.width + spacing
rowHeight = max(rowHeight, size.height)
}
return (placements, y + rowHeight)
}实现 sizeThatFits
sizeThatFits 会针对提议的宽度运行该算法,并返回提议的宽度和计算出的总高度。
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxWidth = proposal.width ?? .infinity
let result = layout(subviews, maxWidth: maxWidth)
return CGSize(width: maxWidth == .infinity ? result.placements.map { $0.point.x + $0.size.width }.max() ?? 0 : maxWidth,
height: result.height)
}实现 placeSubviews
placeSubviews 会运行相同的算法,然后将每个放置位置偏移 bounds.origin,并调用 place。
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let result = layout(subviews, maxWidth: bounds.width)
for p in result.placements {
let pt = CGPoint(x: bounds.minX + p.point.x, y: bounds.minY + p.point.y)
subviews[p.index].place(at: pt, anchor: .topLeading,
proposal: ProposedViewSize(p.size))
}
}完整的 FlowLayout 类型
将这些部分组合起来,就得到了一个完整且可重复使用的流式布局。共享的 layout 辅助方法可以确保尺寸和放置结果一致。
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let r = layout(subviews, maxWidth: proposal.width ?? .infinity)
return CGSize(width: proposal.width ?? 0, height: r.height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let r = layout(subviews, maxWidth: bounds.width)
for p in r.placements {
subviews[p.index].place(at: CGPoint(x: bounds.minX + p.point.x, y: bounds.minY + p.point.y),
anchor: .topLeading, proposal: ProposedViewSize(p.size))
}
}
}使用流式布局
像使用任何容器一样将它放入即可。随着宽度缩小,标签会自动换行。
var body: some View {
FlowLayout {
ForEach(tags, id: \.self) { tag in
Text(tag)
.padding(8)
.background(.blue.opacity(0.2))
.clipShape(Capsule())
}
}
}处理每行的第一个项目
条件 x > 0 可以防止在一行的第一个项目之前换行。没有这个条件时,宽于容器的项目可能会导致无限循环,或者无谓地移到空行中。
if x + size.width > maxWidth, x > 0 {
// wrap only if the row already has content
}间距选择
这里的间距是一个带有默认值的存储属性。在实际应用中,请读取每个子视图的 spacing 偏好,以遵循系统度量值,而不是使用硬编码的常量。
var spacing: CGFloat = 8 // configurable per instance测试边界情况
请使用零个项目、一个超大的项目以及许多很小的项目进行测试。正确的流式布局会为没有内容的情况报告零高度,并且绝不会裁剪单个超大子视图。
快速检查:流式布局
请测试您对换行算法的理解。
回顾:构建流式布局
流式布局通过跟踪不断变化的 x,并在下一个子视图会溢出时重置到新行,从而实现项目换行。共享辅助方法会计算所有放置位置和总高度,让 sizeThatFits 与 placeSubviews 保持一致。
请使用 bounds.origin 偏移放置位置,为每行的第一个项目添加保护条件,并测试空内容、超大项目和大量项目的情况。最终得到的是一个可重复使用的换行容器。
常见问题解答
「构建流式布局」课时是免费的吗?
是的 — 「构建流式布局」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Swift Academy 课程的其余内容,请升级到 CoddyKit PRO。 Swift Academy 课程共包含 4 节课。
「构建流式布局」这节课中我会学到什么?
动态地将项目换到多行显示。 你通过在浏览器中直接运行的动手代码来练习 Swift Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Swift Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Swift Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「构建流式布局」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Swift Academy 课中编写并运行代码吗?
能。每节 Swift Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- Layout 协议基础
- 测量子视图
- 构建流式布局
- 布局缓存与性能