Stacks, Grids, and Outlines in SwiftUI. What and When to Use?

Photo of Patryk Strzemiecki

Patryk Strzemiecki

Updated Dec 6, 2024 • 11 min read
swiftui design

After the last SwiftUI update, there is no short answer from Apple regarding when to use what component when creating a new app or moving from the UIKit.

In this article, I explain the basics of the new and related components presented at WWDC2020 session and suggest when to use them.

LazyStacks


From iOS 14 on, instead of using VStack or HStack and putting one of these components inside a ScrollView, developers may use LazyVStack or LazyHStack. The UI will be similar to using a UITableView in UIKit or a UICollectionView with a single row or column.

Compared to the old solution, the new ‘lazy’ stack view does not load embedded views until it needs to render them on screen, but this works only when a LazyStack is placed inside a ScrollView. Both normal stacks and the ‘lazy’ ones are not scrollable out of the box.

I created a simple View that embeds a LazyVStack in a ScrollView. Based on the KittensModel, it renders a list of KittenViews that are lazily loaded. A single ‘Kitten’ model must be an object or struct that conforms to the Identifiable protocol for SwiftUI to be able to iterate through it in the ForEach.

Implementation

VStack

ScrollView {
    VStack {
        ForEach(kittensModel.aLotOfKittens) { kitten in
            KittenView(kitten: kitten)
        }
    }
}

(this snippet renders a scrollable vertical stack of KittenView's)

VStack

ScrollView {
    LazyVStack {
        ForEach(kittensModel.aLotOfKittens) { kitten in
            KittenView(kitten: kitten)
        }
    }
}

(this snippet renders a scrollable lazy loaded vertical stack of KittenView's)

Performance

The main difference between VStack and LazyVStack components is memory management.

The lazy version loads ‘cells’ only when needed. It depends on the use case, but my app that displays a list of images uses 2.5 times as much memory as when implemented with VStack.

SwiftUI VStack performance
SwiftUI LazyVStack performance

LazyGrids

Similar to Lazy Stacks, starting from iOS 14 we can use Lazy Grids. There was no Grid component in SwiftUI in iOS 13. The main noticeable difference in the API between Stacks and Grids is that the LazyVGrid initializer takes a `columns` argument and LazyHGrid has a `rows` argument. Both rows and columns are arrays of GridItem structures that specify the layout. Same as with Stack and LazyStack, there is no scroll by default.

If LazyGrid contains a Section, we can pin this section’s header or footer to float during scrolling.

SwiftUI LazyGrid

Grid layouts can also adapt to the space available to create a variable number of columns.

[GridItem(.adaptive(minimum: 100, maximum: 200))]

(this snippet creates a single grid item, such as a row or a column, more on use of GridItem here )

Implementation

LazyVGrid

ScrollView {
    LazyVGrid(columns: [GridItem(), GridItem()]) {
        ForEach(0..<languages.count) { index in
            Label(languages[index], image: "")
        }
    }
}

(this snippet renders a lazy loaded vertical grid of Labels)

LazyHGrid

Observe that for LazyHGrid developers must change the default axis of the ScrollView.

ScrollView(.horizontal) {
    LazyHGrid(rows: [GridItem(), GridItem()]) {
        ForEach(0..<languages.count) { index in
            Label(languages[index], image: "")
        }
    }
  }

(this snippet renders a lazy loaded horizontal grid of Labels)

Lists

A List is a container that presents rows of hierarchical data arranged in a single column. It looks like a standard VStack, but in addition it applies separators to views and may contain children that add expandability to our view. Children work when the model passed has a property of the same type as the model itself. A List is scrollable without embedding it into a ScrollView and Lists have separators between items by default.

SwiftUI List

A List provides swipe to delete, reordering, and automatic formatting like separators and navigation indicators.

SwiftUI List onDelete

List contents are always loaded lazily, like LazyVStack, so you don’t need to worry about memory management too much.

Implementation

List(
    kittensModel.aLotOfKittens,
    children: \.children
) { kitten in
    KittenView(kitten: kitten)
}

(this snippet renders a scrollable list/table of KittenView's)

OutlinesGroups

The OutlineGroup structure is used for displaying ForEach views that are nested in groups. OutlineGroups are loaded on demand and items are not allocated in the memory when the group is not expanded. We can use OutlineGroups not embedded in a List to handle multiple kinds of data.

SwiftUI OutlinesGroups

An OutlineGroup is used in Lists that have children, but we can use this component separately. It is not scrollable by default and the UI of the expanded group is different than when a List has children. Also, the collapse animation is different since children are loaded on demand.

Implementation

OutlineGroup(
    kittensModel.aLotOfKittens,
    children: \.children
) { kitten in
    KittenView(kitten: kitten)
}

(this snippet renders a list/table of KittenView's)

DisclosureGroups

A DisclosureGroup is an expandable group of views that is very similar to outlines, but the children are defined differently, as they are not passed in the initializer. These group elements are to be defined only in their ViewBuilders and this adds a way to pass multiple different kinds of views.

SwiftUI DisclosureGroups

Implementation

DisclosureGroup("Group", isExpanded: $isExpanded) {
    TextField("Enter text", text: $text)
    Toggle("toggle", isOn: $isToggleOn)
    Text(text)
}

(this snippet renders a list/group of views passed in the @ViewBuilder content)

When to use what?

Stack and LazyStack may seem to be similar, but the use cases vary.

VStack and HStack should be used when:

  • You don’t know which one to use and/or you don’t need the ‘load on demand’ feature from LazyStack. Apple suggests to always use the normal Stack instead of LazyStack.
  • The amount of embedded views is limited, for example when you need to create a profile screen with multiple segments to be displayed. With VStack and HStack, you can embed up to 10 elements without using ForEach.
VStack {
    ProfileHeaderView()
    ProfileDescriptionView()
    ProfileContactView()
}

(this snippet renders a vertical stack of views passed in the @ViewBuilder content)

You need to load all the items into memory. An example is when you need to create a view comparable to a UITableView or a UIStackView with tiny components inside that will not rely heavily on the device’s memory and don’t need scrolling.
VStack {
    ForEach(0..<languages.count) { index in
        Label(languages[index], image: "")
    }
}

(this snippet renders a vertical stack of Labels)

LazyVStack and LazyHStack should be used when:

  • The number of views inside Stack is unknown, for example in an app that loads content from a server and the number of items returned is unknown or big.
  • Embedded views are heavy or the number of populated views is big, for example in an app with a list of photos where the scrolling experience must be smooth.
  • You need to set a pinned view.
SwiftUI List with children OutlineGroup

DisclosureGroup should be used when:

Summary

Thanks for reading and I hope your project’s iOS Deployment Target will not limit you from using new SwiftUI features for long :)Articles that dive more deeply into those subjects that I suggest reading:

Photo of Patryk Strzemiecki

More posts by this author

Patryk Strzemiecki

Senior iOS Developer
Efficient software development  Build faster, deliver more  Start now!

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business