Stacks, Grids, and Outlines in SwiftUI. What and When to Use?
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)
LazyVStack
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.
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.
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.
A List provides swipe to delete, reordering, and automatic formatting like separators and navigation indicators.
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.
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.
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.
DisclosureGroup should be used when:
- when outline should use binding to a Boolean value that determines the group's expansion state (expanded or collapsed)
Example project: https://github.com/strzempa/SwiftUI-iOS14Playground
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:
- [Building Expandable List with OutlineGroup & DisclosureGroup in SwiftUI 2.0] https://medium.com/@alfianlosari/building-expandable-list-with-outlinegroup-disclosuregroup-in-swiftui-2-0-aa9dda14bbab
- [SwiftUI Tutorial — Lists and Navigation] https://medium.com/swlh/swiftui-tutorial-lists-and-navigation-16e1b4dbb98b
- [Building a menu using List] https://www.hackingwithswift.com/quick-start/swiftui/building-a-menu-using-list
- [SwiftUI’s GroupBox, OutlineGroup, and DisclosureGroup in iOS 14] https://medium.com/better-programming/swiftuis-groupbox-outlinegroup-and-disclosuregroup-in-ios-14-cf9fb127cdc0
- [How to position views in a grid using LazyVGrid and LazyHGrid] https://www.hackingwithswift.com/quick-start/swiftui/how-to-position-views-in-a-grid-using-lazyvgrid-and-lazyhgrid
- [Building Collection Views in SwiftUI with LazyVGrid and LazyHGrid] https://www.appcoda.com/swiftui-lazyvgrid-lazyhgrid/
- [Building Grid Layout Using LazyVGrid and LazyHGrid] https://www.appcoda.com/learnswiftui/swiftui-gridlayout.html
- [How to lazy load views using LazyVStack and LazyHStack] https://www.hackingwithswift.com/quick-start/swiftui/how-to-lazy-load-views-using-lazyvstack-and-lazyhstack
- [TableViews in SwiftUI With LazyVStack] https://medium.com/better-programming/tableviews-in-swiftui-with-lazyvstack-8de9d8bc17ee
- [Build A SwiftUI List App [UITableView]] https://softauthor.com/swiftui-list-section-row/