0Pricing
Swift Academy · Lesson

Building a Flow Layout

Wrap items across lines dynamically.

Building a Flow Layout is a free Swift Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is a Flow Layout?

A flow layout places items left to right, wrapping to a new row when the next item would overflow the available width—like tag chips or word wrapping.

The Plan

We will compute row breaks in a shared helper, return total height from sizeThatFits, and reuse the same computation in placeSubviews. Sharing logic keeps the two methods consistent.

Modeling a Placement

A small struct records where each subview goes. Computing all placements up front makes both protocol methods trivial.

struct Placement {
    let index: Int
    let point: CGPoint
    let size: CGSize
}

The Wrapping Algorithm

Walk the subviews, tracking the current x. If a child would exceed the max width, reset x and move down by the row height plus spacing.

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)
}

Implementing sizeThatFits

sizeThatFits runs the algorithm against the proposed width and returns the proposal width by the computed total height.

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)
}

Implementing placeSubviews

placeSubviews runs the same algorithm, then offsets each placement by bounds.origin and calls 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))
    }
}

The Full FlowLayout Type

Combining the pieces gives a complete, reusable flow layout. The shared layout helper guarantees size and placement agree.

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))
        }
    }
}

Using the Flow Layout

Drop it in like any container. The tags wrap automatically as the width shrinks.

var body: some View {
    FlowLayout {
        ForEach(tags, id: \.self) { tag in
            Text(tag)
                .padding(8)
                .background(.blue.opacity(0.2))
                .clipShape(Capsule())
        }
    }
}

Handling the First Item Per Row

The guard x > 0 prevents wrapping before the first item in a row. Without it, an item wider than the container would loop forever or push to an empty row needlessly.

if x + size.width > maxWidth, x > 0 {
    // wrap only if the row already has content
}

Spacing Choices

Here spacing is a stored property with a default. For production, read each subview’s spacing preference to honor system metrics instead of a hardcoded constant.

var spacing: CGFloat = 8 // configurable per instance

Testing Edge Cases

Test with zero items, one giant item, and many tiny items. A correct flow layout reports zero height for empty content and never clips a single oversized child.

Quick Check: Flow Layout

Test your understanding of the wrapping algorithm.

Recap: Building a Flow Layout

A flow layout wraps items by tracking the running x and resetting to a new row when the next child would overflow. A shared helper computes all placements and total height so sizeThatFits and placeSubviews stay consistent.

Offset placements by bounds.origin, guard the first item per row, and test empty/oversized/many-item cases. The result is a reusable wrapping container.

Frequently asked questions

Is the “Building a Flow Layout” lesson free?

Yes — the full text of “Building a Flow Layout” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.

What will I learn in “Building a Flow Layout”?

Wrap items across lines dynamically. You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Swift Academy?

No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Building a Flow Layout” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Swift Academy lesson?

Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The Layout Protocol Basics
  2. Measuring Subviews
  3. Building a Flow Layout
  4. Layout Cache and Performance
← Back to Swift Academy