Skip to content

Building Custom Layout: Caching (SwiftUI)

A SwiftUI guide for coding agents. Also covers layout cache associated type, makecache updatecache, layout performance tuning, avoid repeated sizethatfits calls, cache subview sizes.

SwiftUI calls a custom Layout's functions many times per layout pass, so repeated measurement gets expensive. The protocol's Cache associated type lets you compute subview sizes once and reuse them.

Define a Cache Type

Cache is Void by default. Declare a nested Cache struct, build it in makeCache, and refresh it in updateCache, which SwiftUI calls when the layout's children change. Skip updateCache and SwiftUI rebuilds via makeCache instead.

struct FlowLayout: Layout {
  struct Cache {
    var sizes: [CGSize] = []
  }

  func makeCache(subviews: Subviews) -> Cache {
    let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
    return Cache(sizes: sizes)
  }

  func updateCache(_ cache: inout Cache, subviews: Subviews) {
    cache.sizes = subviews.map { $0.sizeThatFits(.unspecified) }
  }
}

What the Cache Functions Cannot Know

makeCache and updateCache receive only the Subviews proxy, not the proposed size or bounds. Positions depend on those, so exact placement cannot be precomputed there. Compute positions in sizeThatFits and placeSubviews, where the cache arrives as an inout parameter you may also write to.

Read the Cache During Placement

placeSubviews uses the cached sizes instead of re-measuring every child, and can store extra derived data back into the cache.

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
  var lineX = bounds.minX
  var lineY = bounds.minY
  var lineHeight: CGFloat = 0

  for index in subviews.indices {
    if lineX + cache.sizes[index].width > (proposal.width ?? 0) {
      lineY += lineHeight
      lineHeight = 0
      lineX = bounds.minX
    }

    let position = CGPoint(
      x: lineX + cache.sizes[index].width / 2,
      y: lineY + cache.sizes[index].height / 2
    )

    lineHeight = max(lineHeight, cache.sizes[index].height)
    lineX += cache.sizes[index].width

    subviews[index].place(
      at: position,
      anchor: .center,
      proposal: ProposedViewSize(cache.sizes[index])
    )
  }
}

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-building-custom-layout-caching" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems