How to Easily Create ScrollToTop in SwiftUI

This article contains tips/tricks for creating a scroll to top button in SwiftUI, both with List and ScrollView

Artikel ini berisi tip/trik dalam membuat tombol scroll to top dengan SwiftUI, baik dengan List maupun ScrollView

Scroll to top is an easy way for users to return to the top of a page by pressing a specific button. Typically, this page contains a long list, requiring users to scroll to view more content. When users have scrolled too far, a scroll to top button becomes essential. Without it, users would have to scroll back multiple times to return to the top.

Just like web applications and others, we can create a scroll to top button in SwiftUI. This button is particularly useful when you are using a List or ScrollView.

#Widget/View ScrollViewReader

SwiftUI already provides a default widget/view that can be used for programmatic scrolling, and that is the ScrollViewReader.

@frozen
struct ScrollViewReader<Content> where Content : View

To scroll to a specific view, you need to assign an ID to that widget/view. Once the ID is defined, you can use the scrollTo method of ScrollViewReader to scroll to the widget/view. In general, if you want to scroll to the top of a List or ScrollView, you assign an ID to the topmost view.

#ScrollToTop for List

Here is an example of how to scroll to the top of a List. For this example, we are using the simplest data, which is just numbers from 1 to 100..

struct ContentView: View {
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List {
                    ForEach(1 ... 100, id: \.self) { i in
                        Text("Item number \(i)")
                            .id(i)
                    }
                }
                Button("Move to top") {
                    withAnimation(.easeIn(duration: 1.0)) {
                        proxy.scrollTo(1)
                    }                
                }
            }
        }
        .navigationTitle("Move to top")
    }    
}

In the example above, to scroll to the top, we use scrollTo(1), where 1 is the ID of the first element in the List. This example is straightforward because we can be sure that the ID of the first/topmost element in the List is always 1.

The withAnimation here is used to provide a smoother scrolling experience with animation.

0:00
/
ScrollToTop pada List

#ScrollToTop for ScrollView

To perform a scroll to the top in a ScrollView, it's similar. Here's an example:

struct WithScrollView: View {
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack {
                    ForEach(1 ... 100, id: \.self) { i in
                        Text("Item number \(i)")
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .padding(.vertical, 5)
                            .id(i)
                    }
                }
                .padding()
            }

            Button("Move to top") {
                withAnimation(.easeIn(duration: 1.0)) {
                    proxy.scrollTo(1)
                }
            }
        }
        .navigationTitle("Move to top")
    }
}

In the example provided, we are using a ScrollView with a LazyVStack as its content. LazyVStack is a great choice when dealing with a potentially large number of items because it only creates and loads the views that are currently visible on the screen, which can improve performance and efficiency, especially with long lists of data. This approach is commonly used when creating scrollable views with SwiftUI.

0:00
/
ScrollToTop dengan pada ScrollView

#ScrollToTop with Struct Data

In the previous examples, the data displayed was simple numeric arrays. This time, we will attempt to implement scroll to top with complex data, typically represented as structs.

For the struct data example, we will create a data struct for a Person with properties id and name.

struct Person: Identifiable {
    var id: Int
    var name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

With mock data as follows. The IDs are intentionally created randomly to illustrate that the first ID is not necessarily the smallest or zero.

    private var persons: [Person] = [
        Person(id: 11649, name: "Elmer Oconnell"),
        Person(id: 14, name: "Montgomery Norman"),
        Person(id: 8, name: "Jake Bass"),
        Person(id: 20, name: "Nikodem Garza"),
        Person(id: 31, name: "Ashwin Salinas"),
        Person(id: 79, name: "Jason Mcgowan"),
        Person(id: 1002, name: "Natasha Fernandez"),
        Person(id: 21, name: "Chris Duncan"),
        Person(id: 231, name: "Kimberly Sloan"),
        Person(id: 154, name: "Amber Mcbride"),
        Person(id: 876, name: "Chris Duncan"),
        Person(id: 5041, name: "Chantelle Dyer"),
        Person(id: 3964, name: "Alexa Lucero"),
        Person(id: 805, name: "Timothy Graves"),
    ]

To implement scroll to top with the above data, it is typically structured as follows.

    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List {
                    ForEach(persons) { person in
                        VStack(alignment: .leading) {
                            Text(person.name)
                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies ex id ex posuere blandit. Donec semper ligula ut velit molestie malesuada")
                                .font(.caption)
                                .foregroundColor(.secondary)
                                .multilineTextAlignment(.leading)
                        }
                        .id(person.id)
                    }
                }
                Button("Move to top") {
                    withAnimation(.easeIn(duration: 1.0)) {
                        proxy.scrollTo(0) // ????
                    }
                }
            }
        }
    }

Here, a problem arises because we don't actually know the ID of the topmost element in the List. This is because ForEach, up to now, does not provide the index of the array, so this approach with ForEach(persons) can't be used.

The solution is to use the index of the persons array, which can be obtained using .indices. By using this index, we can scroll to the top element because its ID will always be zero.

In this code, we use persons.indices with the .self key path to loop. Through the indices of the persons array, allowing us to scroll to the top element by using .id(0) and scrollViewProxy.scrollTo(0).

    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List {
                    ForEach(persons.indices, id:\.self) { i in
                        let person = persons[i]
                        VStack(alignment: .leading) {
                            Text(person.name)
                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies ex id ex posuere blandit. Donec semper ligula ut velit molestie malesuada")
                                .font(.caption)
                                .foregroundColor(.secondary)
                                .multilineTextAlignment(.leading)
                        }
                        .id(i) // first index always 0
                    }
                }
                Button("Move to top") {
                    withAnimation(.easeIn(duration: 1.0)) {
                        proxy.scrollTo(0)
                    }
                }
            }
        }
    }

Here is the complete code.

struct Person: Identifiable {
    var id: Int
    var name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

struct WithStructData: View {
    private var persons: [Person] = [
        Person(id: 10, name: "Elmer Oconnell"),
        Person(id: 14, name: "Montgomery Norman"),
        Person(id: 8, name: "Jake Bass"),
        Person(id: 20, name: "Nikodem Garza"),
        Person(id: 31, name: "Ashwin Salinas"),
        Person(id: 79, name: "Jason Mcgowan"),
        Person(id: 1002, name: "Natasha Fernandez"),
        Person(id: 21, name: "Chris Duncan"),
        Person(id: 231, name: "Kimberly Sloan"),
        Person(id: 154, name: "Amber Mcbride"),
        Person(id: 876, name: "Chris Duncan"),
        Person(id: 5041, name: "Chantelle Dyer"),
        Person(id: 3964, name: "Alexa Lucero"),
        Person(id: 805, name: "Timothy Graves"),
    ]
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List {
                    ForEach(persons.indices, id:\.self) { i in
                        let person = persons[i]
                        VStack(alignment: .leading) {
                            Text(person.name)
                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies ex id ex posuere blandit. Donec semper ligula ut velit molestie malesuada")
                                .font(.caption)
                                .foregroundColor(.secondary)
                                .multilineTextAlignment(.leading)
                        }
                        .id(i)
                    }
                }
                Button("Move to top") {
                    withAnimation(.easeIn(duration: 1.0)) {
                        proxy.scrollTo(0)
                    }
                }
            }
        }
    }
}
0:00
/
ScrollToTop pada List dengan data Struct

That's the tip/trick for implementing scroll to top in SwiftUI, whether with List or ScrollView. Happy experimenting, and I hope this little bit is helpful.

GitHub - meshwara/SwiftUI-ScrollToTop
Contribute to meshwara/SwiftUI-ScrollToTop development by creating an account on GitHub.