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()
}
}
}
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:
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
.
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)
}
}
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()
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.
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)
}
}
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:
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")
}
....
}
}
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:
- Compare to SwiftUI List
- For more complex data with multiple columns check out Table
- Learn more about SwiftUI Image View and how to size images
This web site certainly has all of the information aand facts
I wanted concerning this subject and didn’t know who to ask.
My blog Marla
It’s actually a cool and helpful piece of
info. I’m satisfied that youu just shared this helpful info with us.
Please stay us up to date like this. Thank you for sharing.
My webpage Elvera