Swift and SwiftUI tutorials for Swift Developers

VStack vs LazyVStack in SwiftUI

In the world of Swift programming and declarative interface development, managing layout and performance is a constant battle. When Apple introduced SwiftUI, they gave us VStack, a powerful but sometimes misunderstood tool. A year later, in iOS 14, LazyVStack arrived, promising to solve memory issues in long lists.

For an iOS Developer, choosing between these two containers is not just a matter of preference, but of architecture. An incorrect choice can lead to a sluggish application, dropped frames (FPS), and excessive memory consumption. In this technical tutorial, we will break down the critical differences, memory behavior, and specific use cases for VStack vs LazyVStack in SwiftUI and Xcode, covering iOS, macOS, and watchOS.

The Classic Contender: VStack

The VStack (Vertical Stack) is the fundamental building block. Its behavior is “greedy” or eager loading. This means that SwiftUI will render and initialize all views contained within the stack immediately, regardless of whether they are visible on the screen or not.

Imagine you have a list of 1000 complex items. If you use a VStack inside a ScrollView, the system will attempt to create all 1000 rows the moment the view appears. This causes an instant memory spike and a noticeable delay in initial navigation.

When to use VStack?

  • When you have a small, fixed number of items (for example, a login form, a profile card).
  • When you need all views to be alive to maintain their internal state or synchronized animations.
  • When the content does not require scrolling or is very short.

The Modern Challenger: LazyVStack

Introduced in Xcode 12, LazyVStack changes the game. As its name suggests, it is “lazy”. It only renders the views that are currently visible in the viewport (the device screen), plus a small safety margin (buffer) to ensure smooth scrolling.

As the user scrolls, LazyVStack creates new rows and destroys (or recycles, depending on SwiftUI’s internal graph management) those that leave the screen. This is analogous to the behavior of UITableView or UICollectionView in UIKit, but with a much cleaner syntax in Swift.

Practical Experiment: Measuring Lifecycle

The best way for an iOS Developer to understand the difference is through code. Let’s create a simple view that prints to the console when it initializes.

import SwiftUI

struct RowView: View {
    let id: Int
    
    init(id: Int) {
        self.id = id
        print("Initializing RowView \(id)")
    }
    
    var body: some View {
        Text("Row \(id)")
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.blue.opacity(0.1))
            .cornerRadius(8)
    }
}

struct StackComparisonView: View {
    var body: some View {
        ScrollView {
            // CHANGE THIS: VStack vs LazyVStack
            LazyVStack(spacing: 16) {
                ForEach(0..<1000, id: \.self) { i in
                    RowView(id: i)
                }
            }
            .padding()
        }
    }
}

Experiment Results:

  • With VStack: When running the app, you will see in the Xcode console that the “Initializing RowView…” logs print from 0 to 999 instantly. The app will take a moment to respond.
  • With LazyVStack: You will only see the logs for the visible rows (e.g., from 0 to 15). As you scroll, you will see new logs appear. The app launch is immediate.

Critical Layout Differences

Beyond performance, there are layout behavior differences that every expert in Swift programming must know. VStack and LazyVStack do not calculate space in the same way.

1. Width Calculation and Spacers

A VStack calculates the size of all its children before rendering. It knows exactly how wide the widest element is and adjusts the container. A LazyVStack, not knowing the size of elements that haven’t loaded yet, prefers to take up the full available width of the parent container (usually the ScrollView) by default.

Additionally, the use of Spacer() is different. In a VStack, a Spacer will push content to fill the space. In a LazyVStack, a Spacer will not expand unless given an explicit height or the container has a fixed height, since the lazy stack tries to contract vertically to its content.

2. Pinned Views (Sticky Sections)

A feature exclusive to lazy stacks (both LazyVStack and LazyHStack) is the ability to “pin” views, similar to UIKit’s “sticky headers.” The standard VStack does not support this natively.

struct StickyHeaderExample: View {
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 10, pinnedViews: [.sectionHeaders]) {
                Section(header: HeaderView(title: "Section A")) {
                    ForEach(0..<10) { _ in ItemView() }
                }
                
                Section(header: HeaderView(title: "Section B")) {
                    ForEach(0..<10) { _ in ItemView() }
                }
            }
        }
    }
}

struct HeaderView: View {
    let title: String
    var body: some View {
        Text(title)
            .font(.headline)
            .frame(maxWidth: .infinity)
            .padding()
            .background(Color(.systemBackground)) // Important to hide content when scrolling
            .overlay(Divider(), alignment: .bottom)
    }
}

struct ItemView: View {
    var body: some View {
        RoundedRectangle(cornerRadius: 10)
            .fill(Color.orange.opacity(0.2))
            .frame(height: 50)
            .padding(.horizontal)
    }
}

The .onAppear() Event and Lifecycle

This is a common pain point in Swift programming. If you need to load data from the network when a row appears (infinite pagination), you must use LazyVStack.

  • In VStack: The .onAppear modifier of every child fires almost simultaneously at the start, since all views are created at once. It is useless for pagination.
  • In LazyVStack: The .onAppear modifier fires only when the row enters the visible area. This is ideal for detecting when the user reaches the end of the list and requesting more data from the API.

Considerations for macOS and watchOS

As developers using Xcode for the entire ecosystem, we must think about other platforms:

  • watchOS: The Apple Watch has limited resources. Using VStack for long lists can easily block the interface (“hang”). LazyVStack is almost mandatory for collections of more than 20 items on the watch.
  • macOS: Here the window size changes. LazyVStack adapts better to dynamic window resizing than a loaded VStack, which might require recalculating the layout of thousands of invisible items when the window width changes.

When NOT to use LazyVStack?

It might seem that “Lazy” is always better, but it has its downsides. When scrolling fast, the processor must calculate, create, and render views in milliseconds. If your views are extremely complex (many gradients, shadows, complex paths), you might notice small stutters when scrolling fast because the UI thread is saturated creating views.

In those specific cases, if the list is not infinite (say, 50 complex items), a VStack might offer smoother scrolling (“buttery smooth”) because all the heavy lifting was done at the beginning, even if the initial load time is longer.

Conclusion

The choice between VStack vs LazyVStack in SwiftUI is an engineering decision. Do not use LazyVStack by default for everything; use it when you have dynamic, long lists or need “sticky headers.” Use VStack for static layout structures and small containers.

Mastering these differences will make you a better iOS Developer, capable of creating applications in Swift that not only look good but feel fluid and respect the user’s battery and memory.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

SwiftUI Performance Optimization

Next Article

How to know what version of Swift is in Xcode

Related Posts