Layout: Stacks, Lists & ScrollViews

iOS Development Fundamentals

Chapter 7 · Layout: Stacks, Lists & ScrollViews

Every real screen needs to arrange more than one view. This chapter covers the three stack containers, then SwiftUI's real, purpose-built tools for showing many items at once — List and ScrollView.

The Three Stacks

VStack

Arranges children vertically, top to bottom.

HStack

Arranges children horizontally, left to right.

ZStack

Layers children on top of one another, back to front.

VStack(alignment: .leading, spacing: 8) { Text("Title").font(.headline) Text("Subtitle").font(.subheadline) HStack { Text("Left") Spacer() Text("Right") } }
Real, Explicit Alignment & Spacing
Both real parameters shown above — alignment and spacing — are optional, with sensible system defaults if omitted. Spacer() is a real, invisible view that expands to fill all available space along its stack's own axis, pushing the views on either side of it apart — exactly how "Left" and "Right" land at opposite edges above.

List and ForEach

A real List is SwiftUI's own dedicated, scrollable, row-based view — the same visual pattern behind Settings, Mail, and most other built-in Apple apps. For dynamic data, it's paired with ForEach:

struct Task: Identifiable { let id = UUID() let title: String } struct TaskListView: View { let tasks: [Task] var body: some View { List { ForEach(tasks) { task in Text(task.title) } } } }
A Real, Genuine Requirement
ForEach needs a real way to tell each element apart across data updates — either the element's own type conforms to Identifiable (a real protocol requiring one stable id property, as with Task above), or a specific key path is provided explicitly: ForEach(tasks, id: \.title). Skipping both is a real compile error, not a warning — SwiftUI genuinely can't animate or diff a list it can't tell individual rows apart within.
A Real Performance Detail
List is lazy by real, built-in default — rows are created only as they're about to scroll into view, not all at once up front. A list of thousands of rows performs well without any extra work.

ScrollView for Everything Else

List's own row-based, one-item-per-line design doesn't fit every layout — a horizontal photo carousel, or a custom card grid, needs a genuinely different container: ScrollView.

ScrollView(.horizontal) { LazyHStack { ForEach(photos) { photo in PhotoCard(photo: photo) } } }
ContainerReal LazinessUse It For
ListLazy by built-in defaultStandard, scrollable, row-based data — the common real case
VStack/HStackNot lazy — builds every child immediatelyA small, fixed, known number of views
ScrollView + LazyVStack/LazyHStackLazy, explicitly opted intoCustom scrollable layouts List doesn't fit — grids, carousels, non-row designs
Coming From Web Development
List's automatic laziness is conceptually close to what "virtualization" libraries add on top of a plain React list — only rendering what's actually visible. The real difference: it's a genuine, built-in SwiftUI default, not a separate library to reach for.

Hands-On Exercises

Exercise 1

Build a struct Contact: Identifiable with let id = UUID() and let name: String, an array of five sample contacts, and a List/ForEach combination displaying each contact's name.

📄 View solution
Exercise 2

Build a horizontal ScrollView containing a LazyHStack of ten numbered, colored rectangle views. Explain, in your own words, why LazyHStack is the better real choice here over a plain HStack.

📄 View solution
Exercise 3

Explain, in your own words, why ForEach(tasks) compiles fine when Task conforms to Identifiable, but produces a real compile error for a plain struct with no id property and no explicit id: argument.

📄 View solution

Chapter 7 Quick Reference

  • VStack/HStack/ZStack — vertical, horizontal, layered arrangement; Spacer() fills available space along the stack's axis
  • List + ForEach — the standard scrollable, row-based container, lazy by real built-in default
  • ForEach requires Identifiable conformance or an explicit id: key path
  • ScrollView + LazyVStack/LazyHStack — for custom scrollable layouts List's row design doesn't fit
  • Plain VStack/HStack build every child immediately — fine for a small, fixed number of views, not for large or dynamic collections