How to use SwiftUI ScrollView to Create Amazing Collection Views

In this blog post, I will show you how to use SwiftUI ScrollView, including vertical and horizontal scrolling, how to programmatically scroll, and how to create very complex layout views.

ScrollView got a lot of new updates with iOS 17 and macOS 14. These include paging behaviour, containerRelativeFrame, custom scroll bounce behaviour, and reading the scroll position. You can also add scroll animations with scrollTransition and visualEffect.

I will show you a lot of different examples and use cases. So that by the end of this blog, you will want to start using ScrollView in your next iOS project and build amazing-looking UI.

Download the project files from here ⬇️

What is a ScrollView

The scroll view is a scrollable component in SwiftUI. It is a container view that shows its content either in a horizontal or vertical scroll direction. It does not allow to zoom in or out of its content.

What is the difference between ScrollView and List in SwiftUI?

SwiftUI provides developers with two ways to create scrollable content: List and ScrollView. ScrollView is the more versatile option both in terms of custom styling as well as adjusting scroll behavior. For example, ScrollView allows for programmatical scrolling, set a horizontal scroll view, show grid views and create sections with different scroll axis similar to UICollectionView in UIKit.

Showing Large Data Sets

The List view is more efficient when it comes to memory usage due to its ability to lazy load only what is necessary as you scroll, while a ScrollView loads all content at once into memory. When working with large data sets inside a ScrollView, make use of LazyVStack and LazyHStack. These load their data lazily similar to List.

Options for Custom Styling

When it comes to customizing the styling, ScrollView is more versatile while List gives you a good default appearance right out of the box, which is adjusted for the different Apple platforms. If you are looking to make a cross-platform app, you can take advantage of List to quickly create a great-looking UI.

How to add content to SwiftUI ScrollView?

To add content to ScrollView, you can embed your SwiftUI views in a ScrollView. Per default, it will show the subviews like a vertical stack. A typical use case is if you want to fit your content on the iPhone screen, especially for smaller screen sizes or for larger dynamic types. In the following example, I embed the view in a ScrollView, which makes it easily adaptable for most situations:

struct ContentView: View {
    let description = "Lorem ipsum ..."

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                Image("minime")
                    .resizable()
                    .scaledToFill()
                    .frame(width: 350, height: 350)
                    .clipped()

                Text("Mini-me")
                    .font(.largeTitle)

                Text(description)
                    .multilineTextAlignment(.leading)
            }
            .padding()
        }
    }
}
SwiftUI ScrollView used for fiting the content on smaller iphone screens or larger dynamic type

Creating a ScrollView with a Grid Layout

You can add any type of SwiftUI view to the ScrollView. For example LazyVStack, LazyHStack, LazyVGrid, or LazyHGrid. For example, we can make a vertical grid veritcally scrollable:

Example for SwiftUI ScrollView with grid layout using LazyVGrid
struct ContentView: View {
    let columns = [GridItem(.adaptive(minimum: 75.00), spacing: 10)]
      var body: some View {
           ScrollView {
               LazyVGrid(columns: columns) {
                   ForEach(0x1f600...0x1f679, id: \.self) { value in
                       GroupBox {
                           Text(emoji(value))
                               .font(.largeTitle)
                               .fixedSize()
                           Text(String(format: "%x", value))
                               .fixedSize()
                       }
                   }
               }
               .padding()
           }
      }
      private func emoji(_ value: Int) -> String {
          guard let scalar = UnicodeScalar(value) else { return "?" }
          return String(Character(scalar))
      }
}

Example Data

Because you can create some great-looking UI with ScrollView, I am using more complex data, where I added images in the assets catalog (how to work with images):

struct NatureInspiration: Identifiable {
    let name: String
    let imageName: String
    let description: String
    let id = UUID()

    static func examples() -> [NatureInspiration] {
        [NatureInspiration(name: "Desert",
                           imageName: "desert",
                           description: "A desert is a barren area of landscape where little precipitation occurs and, consequently, living conditions are hostile for plant and animal life."),
         ...
        ]
    }
}

I also defined 3 different cells for NatureInspiration to have some variety in the sample project. The following defines a Card view:

struct InspirationCardView: View {

    let inspiration: NatureInspiration
    let padding: CGFloat = 10

    var body: some View {
        Image(inspiration.imageName)
            .resizable()
            .scaledToFit()
            .cornerRadius(10)
            .shadow(radius: 5)

            .overlay(alignment: .bottomTrailing, content: {
                Text(inspiration.name)
                    .bold()
                    .foregroundColor(Color.white)
                    .padding()
            })
    }
}

The size of the images is adjusted to fit the available space by using the resizable modifier and scaleToFit.

swiftui roadmap

Feeling Lost in SwiftUI?

This SwiftUI roadmap shows you what to learn next.

  • Key concepts at a glance
  • Spot your knowledge gaps
  • Guide your learning path

Ideal for beginners and self-learners.

How to change the Scrollable Axis to Horizontal

Scroll view has a parameter to change the scroll direction from the default vertical scrolling to horizontal. We also have to use a horizontal stack inside the ScrollView otherwise the layout directions don’t match. You could use LazyHStack or LazyHGrid.

struct ContentView: View {
    let natureInspiration = NatureInspiration.examples()
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: 10) {
                ForEach(natureInspiration) { inspiration in
                    InspirationCardView(inspiration: inspiration)
                }
            }
            .padding()
        }
        .frame(height: 200)
    }
}
SwiftUI ScrollView examples with horizontal and vertical axis

Complex Layout Example

We can combine multiple ScrollViews together to create a complex layout similar to UICollectionView. It is possible to add ScrollView as a child view to a parent ScrollView. You can create sections with horizontal scroll views by adding the following to the parent view:

ScrollView {
    ScrollView(.horizontal) {
        LazyHStack(spacing: 10) {
            ForEach(natureInspiration) { inspiration in
                InspirationCardView(inspiration: inspiration)
            }
        }

        // Add more sections
    }
}
struct ContentView: View {
    @State private var inspiration = NatureInspiration.examples()
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack(alignment: .leading) {
                    ScrollView(.horizontal) {
                        LazyHStack {
                            ForEach(inspiration) { inspiration in
                                InspirationCardView(inspiration: inspiration)
                            }
                        }
                        .padding()
                    }
                    .frame(height: 180)

                    Section {
                        InspirationGridSectionView()
                    } header: {
                        Text("Second Section")
                            .font(.headline)
                    }

                    LazyVStack(alignment: .leading) {
                        Text("Third Section")
                            .font(.headline)
                        ForEach(inspiration) { inspiration in
                            InspirationRowView(inspiration: inspiration)
                        }
                    }
                    .padding()
                }
            }
            .navigationTitle("Inspirations")
        }
    }
}

How to Customize the Scroll Behaviour

How do I disable scrolling?

With iOS 16, we can now disable scrolling for any scrollable component including ScrollView, List, and TextEditor. Use the new modifier scrollDisabled(_:). You can also dynamically enable/disable this behaviour.

ScrollView {
   ...
}
.scrollDisabled(true)

How to hide the Scroll Indicator

The scroll view displays per default scroll indicators. If you want to hide the scroll indicator, you can pass false for the argument showsIndicators:

ScrollView(showsIndicators: false) {
   ...
}

This changed to scrollIndicators for iOS 16 and macOS 13:

ScrollView {
   ...
}.scrollIndicators(.hidden)

How do I programmatically flash the ScrollView indicator?

New with iOS 17 and macOS 14, you can now programmatically flash the indictor. In the following is an example where the indicator is flashing when the scroll view appears:

ScrollView {
   ...
}
.scrollIndicatorsFlash(onAppear: true)

If you want to trigger the scroll indicator flashing at a specific time, you can use the scrollIndicatorsFlash(trigger:). In the following example, the indicator flashes when ever the value of count changes:

@State private var count: Int = 0

ScrollView {
   ...
}
.scrollIndicatorsFlash(trigger: count)

ScrollView Clipping Behaviour

SwiftUI ScrollView will clip its content when it moves out of its visible area. You can disabled clipping in iOS 17 with:

ScrollView(.horizontal) {
    HStack(spacing: 10) {
        ForEach(natureInspiration) { inspiration in
            InspirationImageView(inspiration: inspiration)
                .frame(width: 300)
        }
    }
}
.scrollClipDisabled()
SwiftUI Scrollview with clipping disabled

Programmatic scrolling with ScrollViewReader

With iOS 14 and macOS 11, ScrollViewReader was introduced, which gives you the ScrollViewProxy. Call scrollTo(_ id: ID, anchor: UnitPoint) to programmatically scroll to the view with the provided id. I

n the following example, I added an id to the last text view in the scroll view. At the top of the scroll view, you can use the button to scroll down. Programmatic scrolling is done by calling the proxis scrollTo(bottomID). Because I make the change in a withAnimation block, the scrolling animates nicely.

struct ContentView: View {

@Namespace var bottomID

var body: some View {
    ScrollViewReader { proxy in
        ScrollView {
            Button("Scroll to Bottom") {
                withAnimation {
                    proxy.scrollTo(bottomID)
                }
            }

            // main content

            Text("You reached the bottom")
                .id(bottomID)
        }
    }
}

Scrolling in a dynamic list

In case you have a dynamic list with many data rows, we can set the id like this:

ScrollViewReader { proxy in
      ScrollView {
         ForEach(natureInspiration) { inspiration in
               InspirationRowView(inspiration: inspiration)
                  .id(inspiration.id)
    }
}

You can then scroll to a specific row by using the data´s id:

Button("Scroll to selected inspiration") {
    withAnimation {
        proxy.scrollTo(selectedInspiration.id, anchor: .top)
    }
}

Notice that I used the anchor parameter, where you have the options for top, center, and bottom. The default anchor is center. If you set an anchor of top, the top of the scroll views frame is aligned to the top of the view you want to scroll to. If the scrollable content region is not large enough. For example, scroll to the last cell and use a top anchor, the scroll view will not scroll fully to the defined position.

Example implementation for scroll to in a SwiftUI ScrollView
struct ContentView: View {
let natureInspiration = NatureInspiration.examples()
@Namespace var topID
@Namespace var bottomID
@State private var selectedInspirationId: UUID? = nil

  var body: some View {
     ScrollViewReader { proxy in
        ScrollView {
            Button("Scroll to Bottom") {
                withAnimation {
                    proxy.scrollTo(bottomID)
                }
            }
            .buttonStyle(.bordered)
            .id(topID)

            LazyVStack(alignment: .leading, spacing: 10) {
                ForEach(natureInspiration) { inspiration in
                    InspirationRowView(inspiration: inspiration)
                        .foregroundColor(forgroundColor(for: inspiration))
                        .background(
                            RoundedRectangle(cornerRadius: 5)
                                .fill(color(for: inspiration)))
                        .id(inspiration.id)
                        .onTapGesture {
                            tapped(on: inspiration)
                        }
                    Divider()
                }
            }.padding()
        }
     }
    .overlay {
        VStack(spacing: 0) {
            Rectangle().stroke(Color.pink, lineWidth: 2)
            Rectangle().stroke(Color.pink, lineWidth: 2)
        }
    }
  }
}

In order to see the alignment better, I added an overlay with red lines. You can see the frame of the scroll view and the center line.

ScrollViewReader { proxy in
    ScrollView {
        ...
    }
}
.overlay {
    VStack(spacing: 0) {
        Rectangle().stroke(Color.pink, lineWidth: 2)
        Rectangle().stroke(Color.pink, lineWidth: 2)
    }
}
SwiftUI scrollview examples where ScrollViewReader is used to scroll to particular row with differnt anchor settings
When you tap on a button the list scrolls programmatically to the selected row (highlighted in grey).

How to get ScrollView offset in SwiftUI?

ScrollView has a native way to give you the content offset with iOS 17 and macOS 14. You need to use scrollPosition modifier and scrollTargetLayout:

struct Emoji: Identifiable {
    let value: Int
    
    var emojiSting: String {
        guard let scalar = UnicodeScalar(value) else { return "?" }
        return String(Character(scalar))
    }
    
    var valueString: String {
        String(format: "%x", value)
    }
    
    var id: Int {
        return value
    }
    
    static func example() -> Emoji {
         Emoji(value: 0x1f600) 
    }
    
    static func examples() -> [Emoji] {
        let values = 0x1f600...0x1f64f
        return values.map {  Emoji(value: $0) }
    }
}

struct ContentView: View {
    
    @State private var scrolledID: Int? = nil
    let emojis: [Emoji] = Emoji.examples()
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(emojis) { emoji in
                    GroupBox {
                        Text("\(emoji.id)")
                        Text(emoji.emojiSting)
                            .font(.largeTitle)
                            .frame(maxWidth: .infinity)
                    }
                }
            }
            .padding()
            // need this to update scrollid when user manually scrolls
            .scrollTargetLayout() 
        }
        .scrollPosition(id: $scrolledID, anchor: .bottom)
        .safeAreaInset(edge: .bottom) {
            Text("scrollposition \(scrolledID ?? 0)")
                .foregroundStyle(.indigo)
                .font(.title)
                .padding(.top)
                .frame(maxWidth: .infinity)
                .background(.thinMaterial)
        }
        .background(Color.indigo)
    }
}

I am setting the scroll position to bottom in this example. Therefore, the scrollID property will give me the emoji’s id at the bottom of the scroll view. I am showing the emoji ID scroll position at the bottom of the screen with the safeAreaInset modifier:

example of scrollview to get the scroll offset

How to pin a section header to the top of a scroll view?

You can use a LazyVStack inside a ScrollView and specify its argument pinnedViews, which has the option to use section headers or footers:

ScrollView {
    LazyVStack(alignment: .leading, spacing: 10, pinnedViews: .sectionHeaders) {
        Section {
            ForEach(inspiration) { inspiration in
                InspirationRowView(inspiration: inspiration)
            }
        } header: {
            SectionHeaderView(title: "First Section")
        }
        ....
    }

}
swiftui roadmap

Feeling Lost in SwiftUI?

This SwiftUI roadmap shows you what to learn next.

  • Key concepts at a glance
  • Spot your knowledge gaps
  • Guide your learning path

Ideal for beginners and self-learners.

Paging Behaviour

Scroll views very commonly have page behavior, which means that when the user releases scrolling the view snaps into the next page position. This is now finally supported with iOS 17.

You can use the scrollTargetBehavior modifier and set it to view aligned. Alternatively, you can use a paging behaviour.

struct PagingScrollView: View {
    let natureInspiration = NatureInspiration.examples()
    
    let rows = [
        GridItem(.fixed(100), spacing: 5),
        GridItem(.fixed(100), spacing: 5),
        GridItem(.fixed(100), spacing: 5)
    ]
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: rows, alignment: .top, spacing: 15) {
                ForEach(natureInspiration) { inspiration in
                    InspirationRowView(inspiration: inspiration)
                        .containerRelativeFrame(.horizontal,
                                                 count: 8,
                                                 span: 7,
                                                 spacing: 15,
                                                 alignment: .leading)
                }
            }
            .scrollTargetLayout(). // tells the scrollview to use these views for the alignment
        }
        .contentMargins(.horizontal, 20)
        .scrollBounceBehavior(.always)
        .scrollTargetBehavior(.viewAligned) // paging behaviour
    }
}

In the above example, I used other new features for ScrollView like customizing its bouncing behavior.

You can add a content margin, which is similar to adding padding inside the scrollview:

ScrollView(.horizontal) {
   ...
.contentMargins(.horizontal, 20)

One of my favorite new features for ScrollView is containerRelativeFrame. In this example, I am using it to make the inspiration view 7 / 8 of the scroll view width. I want to show a little bit of the next column, so that the user understands he can scroll the content:

InspirationRowView(inspiration: inspiration)
    .containerRelativeFrame(.horizontal,
                            count: 8,
                            span: 7,
                            spacing: 15,
                            alignment: .leading)

Conclusion

We have looked into ScrollViews and how to scroll programmatically, disable scrolling, get the current offset, pin headers, and use Introspect to get paging behavior. ScrollViews are a great way to scroll through your content and keep it organized in sections. With a wide range of options, you can customize the look and feel of your app.

Download the project files from here ⬇️

Further Reading:

2 thoughts on “How to use SwiftUI ScrollView to Create Amazing Collection Views”

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form