Layout 协议基础
实现 sizeThatFits 和 placeSubviews。
Layout 协议基础 是 CoddyKit 上的免费 Swift Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Swift Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Swift Academy 课程共包含 4 节课。
为什么需要自定义布局
SwiftUI 自带 HStack、VStack 和 ZStack,但有时您需要实现它们无法表达的排列逻辑,例如径向菜单、流式布局和砌体网格。
布局协议让您可以编写这样的逻辑,同时仍能与普通 SwiftUI 视图组合使用。
布局协议
Layout 要求实现两个方法:sizeThatFits(我有多大?)和 placeSubviews(每个子视图应放在哪里?)。
您可以让值类型实现该协议,并像使用任何内置堆叠容器一样使用它。
struct MyLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) -> CGSize { .zero }
func placeSubviews(in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) { }
}使用自定义布局
Layout 值充当容器。您可以像使用堆叠容器一样,在它的尾随闭包中传入子视图。
var body: some View {
MyLayout {
Text("One")
Text("Two")
Text("Three")
}
}ProposedViewSize
父视图通过 ProposedViewSize 向您的布局提出尺寸建议。它的 width 和 height 是可选值:nil 表示“未指定,请选择理想尺寸”。
特殊值 .zero、.infinity 和 .unspecified 分别用于探测最小尺寸、最大尺寸和理想尺寸。
func sizeThatFits(proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) -> CGSize {
let width = proposal.width ?? 0
return CGSize(width: width, height: 44)
}详细了解 sizeThatFits
sizeThatFits 会根据尺寸建议返回您的布局所需的尺寸。SwiftUI 可能会使用不同的建议多次调用它,以了解您的布局具有多大的适应性。
func sizeThatFits(proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) -> CGSize {
// Sum child ideal sizes for a simple horizontal stack
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
let w = sizes.reduce(0) { $0 + $1.width }
let h = sizes.map(\.height).max() ?? 0
return CGSize(width: w, height: h)
}子视图集合
Subviews 是一个集合,其中包含每个子视图的代理对象。每个代理对象都会回答 sizeThatFits,并公开布局优先级和自定义值。
您永远不会看到真正的视图,只能看到这些测量代理对象。
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
print(size)
}placeSubviews
placeSubviews 会为每个子视图分配一个位置,该位置位于传递给您的 bounds 矩形内。
请对每个子视图代理对象调用 place(at:anchor:proposal:)。
func placeSubviews(in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) {
var x = bounds.minX
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
subview.place(at: CGPoint(x: x, y: bounds.midY),
anchor: .leading,
proposal: .unspecified)
x += size.width
}
}放置时使用锚点
anchor 参数指定子视图的哪个点落在您给定的位置上。.topLeading 使用左上角进行放置;.center 会让子视图以该点为中心。
subview.place(at: CGPoint(x: 0, y: 0),
anchor: .topLeading,
proposal: .unspecified)边界不一定位于原点
bounds 矩形的起点可能不是 (0,0)。请始终相对于 bounds.minX 和 bounds.minY 定位,而不要使用绝对零点。
let startX = bounds.minX // not 0
let startY = bounds.minY最简等间距布局
综合运用这些知识,下面是一个从左到右排列子视图且不留间距的布局。两个方法保持一致:报告的尺寸与放置结果相匹配。
struct SimpleHStack: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
return CGSize(width: sizes.reduce(0){ $0 + $1.width },
height: sizes.map(\.height).max() ?? 0)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var x = bounds.minX
for s in subviews {
let size = s.sizeThatFits(.unspecified)
s.place(at: CGPoint(x: x, y: bounds.minY), anchor: .topLeading, proposal: .unspecified)
x += size.width
}
}
}保持一致是您的责任
SwiftUI 相信您的两个方法会彼此一致。如果 placeSubviews 使用的空间超过 sizeThatFits 报告的空间,子视图可能会被裁剪或重叠。
请让两个方法共享几何计算。
快速检查:布局基础
请测试您对布局协议的掌握程度。
回顾:布局协议基础
Layout 协议将自定义排列逻辑转换为可复用的容器。sizeThatFits 会针对 ProposedViewSize 报告您的尺寸,而 placeSubviews 会使用 place(at:anchor:proposal:) 在 bounds 内放置每个子视图代理对象。
尺寸建议包含可选的尺寸值,边界可能偏离原点,并且两个方法必须在几何计算上保持一致。接下来我们将深入了解如何测量子视图。
常见问题解答
「Layout 协议基础」课时是免费的吗?
是的 — 「Layout 协议基础」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Swift Academy 课程的其余内容,请升级到 CoddyKit PRO。 Swift Academy 课程共包含 4 节课。
「Layout 协议基础」这节课中我会学到什么?
实现 sizeThatFits 和 placeSubviews。 你通过在浏览器中直接运行的动手代码来练习 Swift Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Swift Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Swift Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「Layout 协议基础」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Swift Academy 课中编写并运行代码吗?
能。每节 Swift Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。