SwiftUI: Layout & Interfaces

Nov 18 2021 Swift 5.5, iOS 15, Xcode 13

Part 1: Dynamic View Layout

2. Lazy Stacks

Episode complete

Play next episode

Next
Save for later
About this episode
See versions

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 1. Introduction Next episode: 3. ScrollViewReader

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.