Leave a rating/review
Notes: 02. Lazy Stacks
Update Notes: This course was originally recorded in 2020. It has been reviewed and all content and materials updated as of October 2021.
The first of SwiftUI’s “lazy” views that we’ll be going over are the Stacks.
In the beginning… (that is, for a year after SwiftUI came out in 2019), we had HStacks and VStacks: for stacking views horizontally, and vertically, respectively. A year later, we got new types, with the prefixing word “Lazy”. In this episode, you’ll find out why we really needed them!
In the Starter project, have a look at the Genre.swift file.
Genre is a very simple type. It’s got a name and an array of subgenres, which also just have names: for themselves, and for their parent genre. We’ll be using this list
of genres throughout this part of the course. I just took them from this Wikipedia page.
You can hit option-command-left-arrow to fold them which you might want to do, because they take up about 900 lines of code! Below the list, you can see that both Genre and Subgenre are Hashable, and Identifiable, based on names. That will allow us to easily use ForEach views based on Arrays of them.
Let’s do that in ContentView! You can access this menu with Shift-Command-O, type in part of the name of the type you need, hit return…
…and that’s the fastest way to open files. For the time being, ContentView’s body is just a Text View, showing the time that the Preview last refreshed.
The reason for the “current time view” will become apparent, shortly.Command-click on the Text, and “Repeat” it.
ForEach(/*@START_MENU_TOKEN@*/0 ..< 5/*@END_MENU_TOKEN@*/) { item in
Text(
formatter.string(from: Date())
)
}
…Then, …embed the ForEach in a VStack, and move the formatter in there, because that’s really the scope where it should be created.
var body: some View {
VStack {
let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
} ()
ForEach(/*@START_MENU_TOKEN@*/0 ..< 5/*@END_MENU_TOKEN@*/) { item in
Text(
formatter.string(from: Date())
)
}
}
}
…then, instead of five views, let’s start off with one for every genre.
ForEach(Genre.list) { genre in
Below, I’ve created a Subgenre view, that looks something like ones you’ll find in Apple Music. Let’s add a random one from each genre.
formatter.string(from: Date())
)
genre.subgenres.randomElement()!.view
}
Now, as you can see, the screen isn’t tall enough to show all of the views in the VStack. bThe solution is to embed it in a scroll view.
There’s no menu item for that yet, but you can embed the VStack in anything else, like another VStack, and change that to be a ScrollView. That’s the easiest solution I know of.
Now, you can live preview and it’s working nicely. But, let’s try changing the ForEach to render every single subgenre. FlatMap is the tool for that job.
ForEach(Genre.list.flatMap(\.subgenres)) { subgenre in
subgenre.view
Now, live preview again. I’ve taken it out of this recording, but my computer’s fan is already starting to go crazy. And Xcode is becoming unresponsive.
I can’t even show you this working. So I’ll just take the first 2 genres, using prefix
.
ForEach(Genre.list.prefix(2).flatMap(\.subgenres)) { subgenre in
Now, I can actually run the preview but notice how all of the timestamps are the same!
That demonstrates that all of these views are loaded at once. And they’d have to be re-rendered with every state change your view might have to worry about. Not a good solution! A Lazy Stack is a good solution.
LazyVStack {
Just switching to that might have helped your computer run the preview better, but we can go full-in on this: delete the prefixing.
Genre.list.flatMap
And run the preview! Now, the scrolling is responsive, despite containing nearly a thousand views! And notice the timestamps: you can see that the views in the ForEach are not created until they need to be onscreen!
Now, those times are persistent, so SwiftUI does need to store them, and so you’ll need more memory for more views—but only based on what the user has actually scrolled to.
You may be thinking that this seems a lot like a List. And that’s true: a list is a specialized version of a LazyVStack.
A list is not as flexible, but it does offer some unique functionality (for now), so here’s why you might want to use it: lists have swipe-to-delete capability and reordering.
That’s really about it, but Lists also have some built-in styling, which might work for some of your layouts: they’ve always got horizontal dividers, and their navigation links have built-in chevron icons.
Interestingly, a List also takes a hybrid approach to lazy loading.
var body: some View {
// ScrollView {
List {
let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
} ()
ForEach(Genre.list.flatMap(\.subgenres)) { subgenre in
Text(
formatter.string(from: Date())
)
subgenre.view
}
}
// }
}
The views are lazyily-loaded, so performance is fine, with large data sets, but some of the data is loaded upfront. Note how the times are all the same, now.
And something unique to Stacks, is that they can be Horizontal, instead of Vertical. Just change the ScrollView’s axis to horizontal…
ScrollView(.horizontal)
…and the stack, to an HStack, instead of a VStack.
LazyHStack
In either dimension, Lazy Stacks are great tools to improve performance for large collections. There is not a hard and fast rule for exactly when to switch from non-lazy to lazy. But in general, only switch to Lazy when you notice a performance issue.