SoatDev IT Consulting
SoatDev IT Consulting
  • About us
  • Expertise
  • Services
  • How it works
  • Contact Us
  • News
  • August 22, 2023
  • Rss Fetcher
Photo by Tianyi Ma on Unsplash

When looking into ways to implement UIKit’s setContentOffset(_:animated:) in SwiftUI, I’ve run into quite a few approaches. Some of them — such as using the id for ScrollView and redrawing the whole thing — are fairly creative, though rather excessive in 2023. Here I suggest much easier ways to set SwiftUI ScrollView offset programmatically.

Modern Approach for iOS 17.0+ Beta

For iOS17, Apple has introduced a new modifier scrollPosition(id:anchor:). It works beautifully with LazyVStack and LazyHStack, allowing you to scroll not only to the top or the bottom but to any view inside ScrollView. As the official documentation highlights:

Use the View/scrollTargetLayout() modifier to configure which the layout that contains your scroll targets.

Imagine the goal is to implement auto scroll to the bottom, to the top, and to a specific view inside the ScrollView. First, let’s create a basic viewModel to hold objects that LazyVStack / LazyHStack will be populated with.

import SwiftUI

final class ScrollViewContentModel: ObservableObject {

// MARK: - Properties

var contentItems: [ContentItem] = ContentItem.defaultContent()
}

struct ContentItem: Identifiable {

// MARK: - Properties

var id: Int

var colour: Color

// MARK: - Init

init(id: Int, colour: Color?) {
self.id = id
self.colour = colour ?? .gray
}

static func defaultContent() -> [ContentItem] {
let colours: [Color] = .randomColours()

return colours.enumerated().map { iterator, colour in
ContentItem(id: iterator, colour: colour)
}
}
}

For this article, the objects will only have id and colour. Then, we will need to create a view that holds viewModel and @State variable scrollPosition — that’s how ScrollView offset will be bound to a particular view to scroll to.

struct ContentView: View {

// MARK: - Properties

@StateObject var viewModel = ScrollViewContentModel()

// 1. In the beginning,
// the scroll offset is aligned with the first view of the scroll content
@State private var scrollPosition: Int? = 0

// MARK: - Body

var body: some View {
VStack(spacing: Constant.vstackSpacing) {
buttonView

ScrollView(.vertical) {
LazyVStack {
ForEach(viewModel.contentItems) { item in
rectangleView(colour: item.colour, text: "(item.id)")
.containerRelativeFrame([.vertical, .horizontal])
}
}
// 2. Here we let the system know that
// LazyVStack is layout target
.scrollTargetLayout()
}
// 3. Here we specify that ScrollView should scroll to
// the position of a particular view that is part of the layout target
.scrollPosition(id: $scrollPosition)
.safeAreaPadding(.horizontal, Constant.safeAreaPadding)
}
.safeAreaPadding(.bottom, Constant.safeAreaPadding)
}
}

Then let’s create three buttons that will tell ScrollView to change its position — scroll to the very top, the bottom, or to the next view.

    private var buttonTop: some View {
Button(action: {
// 1. By making scrollPosition = 0,
// we specify that scrollView should scroll up
// to the first view of the target layout
scrollPosition = 0
}, label: {
Image(systemName: "arrow.up")
.foregroundColor(.white)
})
.padding(.horizontal, Constant.safeAreaPadding)
}

private var buttonBottom: some View {
Button(action: {
// 2. Here we specify that scrollView should scroll down
// to the last view of the target layout
scrollPosition = viewModel.contentItems.count - 1
}, label: {
Image(systemName: "arrow.down")
.foregroundColor(.white)
})
.padding(.horizontal, Constant.safeAreaPadding)
}

// 3. Adding another button for scrolling to the next view
private var nextViewButton: some View {
Button("Next page") {
scrollPosition = scrollPosition == nil ? 0 : (scrollPosition! + 1)
}
}

// 4. Incorporating two buttons into a single view
// that is added to the body
private var buttonView: some View {
ZStack {
Color.black
HStack {
buttonTop
VStack {
nextViewButton
}
.padding(5)

if let position = scrollPosition {
Text("Current page: (position)")
.foregroundColor(.white)
}
}
buttonBottom
}
}
.frame(height: Constant.buttonViewHeight)
}

Here’s the result:

The source code for this example lives here.

ScrollViewReader for iOS 14.0+

To support versions below iOS17, ScrollViewReader to the rescue! SwiftUI’s ScrollViewReader is a view that works with a proxy, adding scroll offset to show child views. Let’s recreate the previous example with ScrollViewReader. At first, we need to adapt buttons to receive ScrollView proxy.

    // 1. Passing proxy to the buttonView to be able to call proxy.scrollTo(id)
private func buttonView(with proxy: ScrollViewProxy) -> some View {
ZStack {
Color.black
HStack {
// 2. Passing proxy to the top button
topButton(with: proxy)

VStack {
Button("Next page") {
// 3. When 'Next page' button is tapped,
// scroll should move to the child's id
if currentId < viewModel.contentItems.count - 1 {
proxy.scrollTo(currentId + 1)
currentId = currentId + 1
}
}
.padding(5)

Text("Current page: (currentId)")
.foregroundColor(.white)
}

// 4. Paassing proxy to the bottom button
bottomButton(with: proxy)
}
}
.frame(height: Constant.buttonViewHeight)
}

private func topButton(with proxy: ScrollViewProxy) -> some View {
Button(action: {
// When tapped, it scrolls to the first child view
proxy.scrollTo(0)
currentId = 0
}, label: {
Image(systemName: "arrow.up")
.foregroundColor(.white)
})
.padding(.horizontal, Constant.safeAreaPadding)
}

private func bottomButton(with proxy: ScrollViewProxy) -> some View {
Button(action: {
// When tapped, it scrolls to the last child view
let lastItemId = viewModel.contentItems.count - 1
proxy.scrollTo(lastItemId)
currentId = lastItemId
}, label: {
Image(systemName: "arrow.down")
.foregroundColor(.white)
})
.padding(.horizontal, Constant.safeAreaPadding)
}

As we tell ScrollView to base its offset off the views ids, we must set these ids for every child view. Let’s add them in ContentView:

    
// MARK: - Properties

@StateObject var viewModel = ScrollViewContentModel()

@State var currentId: Int = 0

// MARK: - Body

var body: some View {
VStack(spacing: Constant.vstackSpacing) {
ScrollViewReader { proxy in
buttonView(with: proxy)
ScrollView(.vertical) {
LazyVStack {
ForEach(viewModel.contentItems) { item in
rectangleView(colour: item.colour, text: "(item.id)")
.containerRelativeFrame([.vertical, .horizontal])
// Seting id for every child view
// to let scrollView be based off it
.id(item.id)
}
}
}
.scrollDisabled(true)
.safeAreaPadding(.horizontal, Constant.safeAreaPadding)
}
}
.safeAreaPadding(.bottom, Constant.safeAreaPadding)
}

This code will allow us to achieve the same auto-scrolling behaviour as if using the latest scrollPosition(id:anchor:).

The source code for this example is in this GitHub repository.

Happy coding!


Scroll Programmatically With SwiftUI ScrollView was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.

Previous Post
Next Post

Recent Posts

  • Computing the Euler-Mascheroni Constant
  • Golden ratio base numbers
  • Pioneering Apple engineer Bill Atkinson dies at 74
  • Lawyers could face ‘severe’ penalties for fake AI-generated citations, UK court warns
  • At the Bitcoin Conference, the Republicans were for sale

Categories

  • Industry News
  • Programming
  • RSS Fetched Articles
  • Uncategorized

Archives

  • June 2025
  • May 2025
  • April 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • July 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023

Tap into the power of Microservices, MVC Architecture, Cloud, Containers, UML, and Scrum methodologies to bolster your project planning, execution, and application development processes.

Solutions

  • IT Consultation
  • Agile Transformation
  • Software Development
  • DevOps & CI/CD

Regions Covered

  • Montreal
  • New York
  • Paris
  • Mauritius
  • Abidjan
  • Dakar

Subscribe to Newsletter

Join our monthly newsletter subscribers to get the latest news and insights.

© Copyright 2023. All Rights Reserved by Soatdev IT Consulting Inc.