Implementing a ScrollView in SwiftUI | SwiftUI Bootcamp #15
ScrollView is how you handle content that exceeds the screen size — it's the container behind every feed, list of cards, and horizontal carousel in iOS apps. After this lesson you'll know how to build both vertical and horizontal scroll views, combine them for a Netflix-style layout, and use LazyVStack/LazyHStack to keep performance fast with large datasets.
What You'll Learn
- How
ScrollViewworks and how to configure vertical vs. horizontal scrolling - Why
LazyVStackandLazyHStackare critical inside scroll views for performance - How to compose nested scroll views to build the "rows of horizontal carousels" layout pattern
Mental Model
Think of a ScrollView like a porthole window on a long train. The porthole (the screen) stays in one place, but the train (the content) slides past it. The content can be much longer than the porthole — the scroll view just manages the sliding.
LazyVStack vs VStack inside a scroll view is like the difference between a real physical train (where only the cars near the porthole exist and the rest are summoned as you scroll) versus a virtual reality simulation (where every single car in a 10,000-car train is rendered at full detail the moment you hit play). The lazy version creates view objects only when they're close to the visible area, which is critical for long lists.
Detailed Explanation
ScrollView wraps any content and makes it scrollable. By default, it scrolls vertically. For horizontal scrolling, pass .horizontal as the first argument. showsIndicators: false hides the scroll bar glyph.
Inside a ScrollView, use LazyVStack instead of VStack and LazyHStack instead of HStack. The "lazy" variants defer view creation: they only instantiate views that are near the current scroll position. A regular VStack inside a scroll view creates all child views immediately, regardless of whether they're visible — with 100+ items, this causes noticeable startup lag and memory spikes.
The nested scroll view pattern in the sample (a vertical scroll with horizontal carousels in each row) is exactly what apps like Netflix, Spotify, and App Store use. The outer ScrollView scrolls vertically through rows; each row contains an inner ScrollView(.horizontal) with cards. The key is using LazyVStack for the outer rows so that rows are only built as you scroll down.
ScrollView doesn't automatically constrain its children's size in the scrolling direction — the content can be as tall (or wide) as it needs to be. This is what enables scrollable content: unlike a VStack which is bounded by its parent, a ScrollView's content can be arbitrarily large.
Code Structure
The sample is in ScrollViewBootcamp.swift and builds a vertical LazyVStack of 100 rows, each containing a horizontal LazyHStack scroll view with 20 cards. This nested structure demonstrates the "carousel of carousels" layout pattern. Each card is a RoundedRectangle with a shadow.
Complete Code
ScrollViewBootcamp.swift
import SwiftUI
struct ScrollViewBootcamp: View {
var body: some View {
ScrollView { // vertical scroll view — wraps the LazyVStack so all rows can scroll
LazyVStack { // only renders rows near the current scroll position — critical for 100 rows
ForEach(0..<100) { index in
ScrollView(.horizontal, showsIndicators: false, content: { // each row scrolls independently
LazyHStack { // only renders cards near the horizontal scroll position
ForEach(0..<20) { index in
RoundedRectangle(cornerRadius: 25.0)
.fill(Color.white) // white card background
.frame(width: 200, height: 150) // fixed card size
.shadow(radius: 10) // drop shadow to elevate cards off background
.padding() // spacing between cards in the horizontal row
}
}
})
}
}
}
}
}
struct ScrollViewBootcamp_Previews: PreviewProvider {
static var previews: some View {
ScrollViewBootcamp()
}
}Code Walkthrough
- Outer
ScrollView {}— The vertical scroll container. It wraps the entireLazyVStack, enabling the user to scroll down through all 100 rows. Without this, theLazyVStackwould attempt to render all rows in a fixed-height container, clipping most of them. LazyVStack— Defers creation of rows until they're near the screen. With 100 rows, only the ~10 visible rows (plus a small buffer above and below) are live at any time. This is what prevents the initial 100-row render from freezing the app.ForEach(0..<100)— Generates 100 row containers. Each iteration creates an independent horizontal scroll view.ScrollView(.horizontal, showsIndicators: false)— Each row is its own horizontal scroll view.showsIndicators: falsehides the scroll bar so the UI looks clean. Each row scrolls independently — swiping left in row 3 doesn't affect row 5.LazyHStack— Inside the horizontal scroll view,LazyHStackensures only the cards near the current horizontal position are rendered. WithoutLazy, all 20 cards in every visible row would be rendered eagerly.RoundedRectangle(...).frame(width: 200, height: 150)— Each card is 200×150pt. The horizontal scroll view allows cards to overflow the screen width — scrolling reveals cards out of the viewport..padding()— Adds spacing around each card. Inside aLazyHStack, this creates visible gaps between cards in the row.
Common Mistakes
Mistake: Using VStack inside ScrollView instead of LazyVStack for long lists A regular VStack with ForEach(0..<1000) inside a ScrollView creates 1,000 views immediately on appear. For datasets of ~50 items or more, always use LazyVStack or LazyHStack. The swap is a one-word change with a potentially massive performance improvement.
Mistake: Putting a ScrollView inside a VStack without a fixed height and watching it collapse A ScrollView inside a VStack without a .frame(height:) may collapse to zero height because the VStack gives it minimal space. Either give the scroll view a fixed frame or use it as a full-screen container. For screen-filling scroll views, make ScrollView the root view, not a child of a VStack.
Mistake: Using two-finger scroll in the simulator and thinking horizontal scrolling is broken The simulator's touchpad scroll gesture controls the vertical scroll view. To test horizontal scroll views in the simulator, click and drag with the mouse cursor sideways on the horizontal row. On a real device, finger swipes work as expected.
Key Takeaways
- Always use
LazyVStack/LazyHStackinsideScrollView— they defer view creation to near-viewport items and are essential for good performance - Nested scroll views (vertical outer + horizontal inner) create the carousel-of-carousels pattern used by major apps
ScrollViewdoesn't constrain its content's size in the scroll direction — that's what enables infinite-length content
Last updated: June 27, 2026