SwiftUI: Layout & Interfaces

Nov 18 2021 Swift 5.5, iOS 15, Xcode 13

Part 1: Dynamic View Layout

4. Challenge: Nested Scrolling Stacks

Lesson Complete

Play Next Lesson
Next
Save for later
About this episode
See versions
See forum comments
Cinema mode Mark as Complete Download course materials
Previous episode: 3. ScrollViewReader Next episode: 5. Grids

Update Notes: This course was originally recorded in 2020. It has been reviewed and all content and materials updated as of October 2021.

In Apple’s Music app, several views, like the ones for “Browsing”, and “Radio”, use layouts like this. They’re based around scrolling in Lazy Vertical Stacks, to make some kind of category visible.

And when you get to that category, then you scroll horizontally, to find a subcategory. It’s a layout option that’s useful for many, many apps. And in this challenge, you’re going to replicate it.

You’ll go from this, to this! The first “this” is just a “Divider”, which is a simple view that draws a separating line.

For the end result, you’ll be making use of the same genres, and subgenre views, that you did in the last two episodes. For every genre, make it so that you can horizontally scroll through all of its subgenres.

And there’s a menu button set up already. Once you’ve got the scroll views in-place, make it so that the menu choice scrolls the vertical stack to the selected genre. Now, pause the video, and have fun with your challenge. I’ll go do it too, and then we can compare results!

Welcome back. For this challenge, there were several steps to take, and you could have taken them in various orders. Here’s a choice that I thought was pretty good for visualizing all the pieces coming together.

First, I assigned a single genre to work with. Randomly.

    NavigationView {
      let genre = Genre.list.randomElement()!

Then, I put the Divider in a LazyVStack.

LazyVStack {

And I added a Text view, with the genre’s name.

      LazyVStack {
        Text(genre.name)
        Divider()
      }

There was no reason for the Stack to be Lazy, with just those two views, but I knew it was about to get much taller. Next, a Lazy H Stack…

        Text(genre.name)

        LazyHStack {

        }

        Divider()

…for all of the subgenres.

        LazyHStack(spacing: 20) {
          ForEach(genre.subgenres, content: \.view)
        }

And, of course, that needed to be horizontally scrollable.

        ScrollView(.horizontal) {
          LazyHStack {
            ForEach(genre.subgenres, content: \.view)
          }
        }

For styling, I set up a horizontal padding constant.

    NavigationView {
      let genre = Genre.list.randomElement()!
      let horizontalPadding: CGFloat = 40

      LazyVStack {

Then I leading-aligned the Stack, so that the Text would move to the edge…

LazyVStack(alignment: .leading) {

…and, I padded it.

        Text(genre.name)
          .padding(.leading, horizontalPadding)

I also gave it a heavier font.

        Text(genre.name)
          .fontWeight(.heavy)
          .padding(.leading, horizontalPadding)

Then I used the same padding on the leading edge of the H Stack…

            ForEach(genre.subgenres, content: \.view)
          }
          .padding(.leading, horizontalPadding)
        }

…and, both horizontal edges of the divider.

        Divider()
          .padding(.horizontal, horizontalPadding)
      }

The reason I couldn’t use padding on the entire VStack, is that I wanted the scroll view to extend to the left edge of the view.

That way, the subgenre views could look like they were scrolling off the left side of the screen. I didn’t want that horizontal scroll indicator, so I set showsIndicators to false, on the scroll view.

ScrollView(.horizontal, showsIndicators: false) {

And I spaced out the subgenres.

LazyHStack(spacing: 20) {

I also added a little bit of space between them, and the divider.

        Divider()
          .padding(.horizontal, horizontalPadding)
          .padding(.top)
      }

Being pretty happy with the view for a single genre, I moved on to rendering the same view for all of them.

      LazyVStack(alignment: .leading) {
        ForEach(Genre.list) { genre in
          Text(genre.name)
            .fontWeight(.heavy)
            .padding(.leading, horizontalPadding)

          ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 20) {
              ForEach(genre.subgenres, content: \.view)
            }
            .padding(.leading, horizontalPadding)
          }

          Divider()
            .padding(.horizontal, horizontalPadding)
            .padding(.top)
        }
      }

I didn’t need a random genre anymore…

    NavigationView {
      let horizontalPadding: CGFloat = 40

But I did need a ScrollView, to be able to view all of them.

    NavigationView {
      ScrollView {
        let horizontalPadding: CGFloat = 40

I padded the top…

      }
      .padding(.top)
      .navigationBarTitleDisplayMode(.inline)

…And I was pretty happy with the result. I just needed to make the Genre button do something.

I added a scroll view reader, to encompass the VStack.

      ScrollView {
        ScrollViewReader { proxy in
          let horizontalPadding: CGFloat = 40

And I added an onChange modifier, just like in the last episode.

                .padding(.top)
            }
          }
          .onChange(of: selectedGenre) { genre in

          }

I decided to scroll to the top of the genre, with animation.

          .onChange(of: selectedGenre) { genre in
            withAnimation {
              proxy.scrollTo(genre, anchor: .top)
            }
          }

And like we went over in the last episode, I made sure to set my selection to nil, afterwards.

            withAnimation {
              proxy.scrollTo(genre, anchor: .top)
            }

            selectedGenre = nil

As for what view to mark as corresponding to the genre, the Text view was the right choice.

                .padding(.leading, horizontalPadding)
                .id(genre)

              ScrollView(.horizontal, showsIndicators: false) {

And with that, I had the makings of a nice view for navigating through two dimensions of collections. If you’re feeling comfortable with lazy stacks now, nice work! You’re ready to move on.