0Pricing
Swift Academy · 课时

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 反馈 — 无需本地设置。

此课程中的所有课时

  1. Layout 协议基础
  2. 测量子视图
  3. 构建流式布局
  4. 布局缓存与性能
← 返回 Swift Academy