Error scroll view did scroll is scroll to top swift năm 2024

SwiftUI is Apple’s new UI building framework released in 2019 as part of the iOS13 update. Compared to the old UIKit, SwiftUI is a declarative, functional framework, allowing developers to build user interfaces much faster, while offering a very useful toolkit to debug and test the result through interactive previews and built-in support from Xcode.

DoorDash’s journey with SwiftUI required figuring out how to programmatically scroll ScrollViews starting with iOS13 to pave the way for more complicated functionalities down the road, including such things as adding snapping-to-content behavior.

ScrollViews are common in any modern UI. They are part of many different use cases and user interfaces, allowing users to experience much more content than would normally fit on a mobile device’s screen or in the given viewport of a web browser.

ScrollViews, as shown in Figure 1, are tightly integrated into our designs to allow us to focus the users’ attention on a particular item. We can use them to highlight position or progress, align content with the viewport, and enforce snapping behavior among myriad other benefits.

Error scroll view did scroll is scroll to top swift năm 2024
Figure 1: The ScrollView is the area below the fixed height view on a mobile app and can, via scrolling, provide access to more content than would otherwise fit on the page.

With our new SwiftUI app, we sought to add many of these features to our app experience. Unfortunately, the first release of SwiftUI in 2019, bundled with iOS13, lacked any means to programmatically move

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

4, which could only be accomplished following the next release for iOS14 a year later.

Building programmatic scrolling

We first will explore the scrollview implementation options before diving into how to implement the programmatic scrolling behavior.

Programmatic scrolling and its benefits

Programmatic scrolling refers to the possibility of instructing the

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 to move to a particular position (referred to as content offset) or to a target view.

Programmatic scrolling was introduced in the first version of iOS (iPhoneOS 2.0) within UIKit. There are publicly available APIs to accomplish it: all

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

6 and their subclasses come with the methods

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

7 and

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

8.

Because of this UIKit legacy, there are countless user experiences already in production offering programmatic scrolling; it has been a core feature for many user flows. As a result, it was only natural to require any new UI framework to enable the same UX.

To review a few examples, our UX design specifies scenarios where a

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 is positioned around some of its subviews, such as scrolling to incomplete sections on a form before the user can submit it:

Error scroll view did scroll is scroll to top swift năm 2024
Figure 2. This form scrolls to the first incomplete section before letting the user save their options

Similarly, when two elements’ scroll positions are linked and scroll together, such as a main list and a horizontal carousel:

Subscribe for weekly updates

Error scroll view did scroll is scroll to top swift năm 2024
Figure 3. The horizontal carousel on the top and the main scroll offset of the menu are linked: scrolling the main menu updates the carousel and tapping any of the labels on the carousel scrolls the main menu.

SwiftUI 1.0

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

4 lacked any kind of support for accomplishing programmatic scrolling in general: there weren’t any APIs that would allow someone to instruct the

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 to move itself to a particular position.

Without out-of-the-box-support, it seemed most expedient to keep the implementation based on UIKit and not take on the extra challenge of using SwiftUI. However, DoorDash’s philosophy has been to investigate new possibilities to move toward our end goal and try to bridge any gaps we encounter along the way. This project was no exception.

Before we show the detailed steps of our investigation, we would like to note that SwiftUI 2.0 shipped with added programmatic support for iOS14, including Apple’s introduction of a ScrollViewReader and a ScrollViewProxy.

The ScrollView reader is a transparent container that exposes a proxy argument to its content. The content’s code blocks can use this proxy to send messages to the

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 to move its offset to a particular position.

However,

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

3 and its proxy are not backward-compatible with iOS13/SwiftUI 1.0.

This improvement nonetheless gave us inspiration for our own API design, including how the end result should look and which syntax feels most natural when building our user interface with SwiftUI.

Building programmatic ScrollViews with SwiftUI

To build something like this on our own required the following steps:

  1. Exposing a reference to the underlying UIKit

    struct ScrollReader<ScrollViewContent: View>: View {

    private let content: (ScrollProxyProtocol) -> Content  
    private let proxy = __ScrollProxy()  
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {  
        self.content = content  
    }  
    var body: some View {  
        content(proxy)  
            .background(  
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })  
            )  
    }  
    
    }

    4. This reference enables us to use the UIKit APIs to programmatically scroll the content.
  2. Building SwiftUI-based components that make use of the above reference so that developers can use these to instruct the

    struct ScrollViewBackgroundReader: UIViewRepresentable {

    let setProxy: (ScrollProxyProtocol) -> ()  
    func makeCoordinator() -> Coordinator {  
        let coordinator = Coordinator()  
        setProxy(coordinator)  
        return coordinator  
    }  
    func makeUIView(context: Context) -> UIView {  
        UIView()  
    }  
    func updateUIView(_ uiView: UIView, context: Context) { }  
    
    }

    5 to scroll.
  3. Wrapping the solution in a convenient and easy-to-understand API to hide its complexity.

For reference, here is a quick code example using Apple’s solution. This is an example of a simple view declaration demonstrating the use of a

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

3. The reader’s content itself is given a parameter of the type

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

7, which exposes the API to scroll the

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 to any designated child view using its assigned identifier:

import SwiftUI
struct ContentView: View {
    enum ScrollPosition: Hashable {
        case image(index: Int)
    }
    var body: some View {
        ScrollView {
            ScrollViewReader { proxy in
                VStack {
                    ForEach(0..<10, content: logoImage)
                    Button {
                        withAnimation {
                            proxy.scrollTo(
                                ScrollPosition.image(index: 0),
                                anchor: .top
                            )
                        }
                    } label: {
                        Text("Scroll to top!")
                    }
                    .buttonStyle(.redButton)
                }
            }
        }
    }
    func logoImage(at index: Int) -> some View {
        Image("brand-logo")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .padding()
            .border(Color.red.opacity(0.5))
            .padding()
            .id(ScrollPosition.image(index: index))
    }
}

Error scroll view did scroll is scroll to top swift năm 2024

We wanted to ensure developers could use a familiar syntax, thus it became our goal to mimic this construction.

Building the components for programmatic scrolling

To implement any kind of custom programmatic API, we had to instruct the UI framework to do what we wanted it to do.

Behind the scenes, SwiftUI components use the old UIKit views as building blocks; in particular

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

4 are using an underlying

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4. If we could safely walk through the UIKit view hierarchy that SwiftUI automatically generated for us to find this component, we could use the old UIKit methods to perform the programmatic scrolling and accomplish our original goal. Assuming we can accomplish this, this should be our path forward.

Sidenote: There are third-party libraries that claim to hook into

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4, but in our experience they do not work reliably across different versions of SwiftUI. That’s why we implemented our own means to locate this reference.

To find a particular superview — in our case the containing

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 — in the UIKit view tree, we need to insert a transparent view which does not actually display anything to the user, but rather is used to investigate the view hierarchy. To this end, we need a

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

3 and a

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

4 instance as its view type.

A short note for the sake of bikeshedding about type names in this example: We call the replacement for Apple’s

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

3 “

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

6’ and for

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

7 we use a protocol called

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

8 and the type of the object we use to implement it as

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

9.

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

Going forward, the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

0 we added as part of this UIViewRepresentable will be doing the heavy lifting for us, including implementing the necessary steps to programmatically scroll the

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4. We pass a closure from the ScrollReader to the background view, so the

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

9 implementation can delegate requests to the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

0. You can see its implementation below.

We can add this reader view as a background to the content of our own

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

6:

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

Throughout this example, we use the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

5 modifier to add our reader views. This allows us to add them as part of the view hierarchy. Furthermore, .background() components share the geometry (position and size) of the receiver, which makes it useful to find the coordinates of various views later on when we need to translate the content’s position to

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

6 coordinates.

Next, we define our

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

8:

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

We shall implement this protocol above with the proxy object (i.e. the

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

9 private type) and separately with the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

0 of the reader. In this design, the proxy object will delegate the scroll requests to the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

0 behind the scenes. The

final class Coordinator: MyScrollViewProxy {
    ...
    private func locateTargetOffset(with identifier: AnyHashable, anchor: UnitPoint) -> (view: UIView, offset: CGPoint)? { 
        // TODO: locate target views with the given identifier
    }
    // MARK: ScrollProxyProtocol implementation
    func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
...

1 reference is passed to the proxy object using the

final class Coordinator: MyScrollViewProxy {
    ...
    private func locateTargetOffset(with identifier: AnyHashable, anchor: UnitPoint) -> (view: UIView, offset: CGPoint)? { 
        // TODO: locate target views with the given identifier
    }
    // MARK: ScrollProxyProtocol implementation
    func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
...

2 closure used above.

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

Our

final class Coordinator: MyScrollViewProxy {
    ...
    private func locateTargetOffset(with identifier: AnyHashable, anchor: UnitPoint) -> (view: UIView, offset: CGPoint)? { 
        // TODO: locate target views with the given identifier
    }
    // MARK: ScrollProxyProtocol implementation
    func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
...

1 implementation of the proxy protocol will look like this code sample below — with

final class Coordinator: MyScrollViewProxy {
    ...
    private func locateTargetOffset(with identifier: AnyHashable, anchor: UnitPoint) -> (view: UIView, offset: CGPoint)? { 
        // TODO: locate target views with the given identifier
    }
    // MARK: ScrollProxyProtocol implementation
    func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
...

4 placeholders for now:

final class Coordinator: MyScrollViewProxy {
    ...
    private func locateTargetOffset(with identifier: AnyHashable, anchor: UnitPoint) -> (view: UIView, offset: CGPoint)? { 
        // TODO: locate target views with the given identifier
    }
    // MARK: ScrollProxyProtocol implementation
    func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
...

As a next step, we need to locate the target content offset based on the view identifier and the UnitPoint anchor before locating the correct

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4 instance.

These two tasks are related; once we have found the correct destination view, we can find the first of its parents, which is also a

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4. To simplify this step, we have added a computed property on

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

4:

extension UIView {
    var enclosingScrollView: UIScrollView? {
         sequence(first: self, next: { $0.superview })
            .first(where: { $0 is UIScrollView }) as? UIScrollView
    }
}

But we still need to identify and locate the target view. In their own solution, Apple is using the SwiftUI

final class Coordinator: MyScrollViewProxy {
    ...
    private func locateTargetOffset(with identifier: AnyHashable, anchor: UnitPoint) -> (view: UIView, offset: CGPoint)? { 
        // TODO: locate target views with the given identifier
    }
    // MARK: ScrollProxyProtocol implementation
    func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
        // TODO: locate the view with the identifier, scroll its parent scrollview to the view’s position
    }
...

8 API to uniquely identify views. This mechanism is used in their programmatic scrolling solution as well.

We cannot use the results of this API because it is private and hidden from us. What we can do is implement something similar.

Annotating target views

Here we use the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

5 modifier again to annotate potential scroll-to targets with a unique identifier while using the background reader view above to locate views with these unique identifiers. To do so, we need to complete the following tasks:

  • Add a SwiftUI API to annotate views
  • Add a lookup mechanism to find these views later when we need to scroll programmatically
  • Convert the placements of these views to

    struct ScrollReader: View {

    ...  
    private final class __ScrollProxy: ScrollProxyProtocol {  
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance  
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {  
            other?.scroll(to: identifier, anchor: anchor)  
        }  
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {  
            other?.scroll(to: identifier, anchor: anchor, offset: offset)  
        }  
    }  
    
    }

    6 content offset coordinates

For the first step, we need to place one more

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

3 in the view hierarchy using the

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

5 modifier:

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

We then add a convenience method to use the above:

extension View {
    /// Marks the given view as a potential scroll-to target for programmatic scrolling.
    ///
    /// - Parameter id: An arbitrary unique identifier. Use this id in the scrollview reader's proxy
    /// methods to scroll to this view.
    func scrollAnchor(_ id: AnyHashable) -> some View {
        background(ScrollAnchorView(id: id))
    }
}

We made sure the

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

3 view and its

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

4 share the same unique ID because the ID’s value is specified in the SwiftUI domain. We will, however, need to locate the

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

4 with the same ID in the UIKit hierarchy.

We can use the following methods to locate the unique

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

4 in the view hierarchy with the given identifier using a recursive lookup:

extension UIView {   
    func scrollAnchorView(with id: AnyHashable) -> UIView? {
        for subview in subviews {
            if let anchor = subview.asAnchor(with: id) ?? subview.scrollAnchorView(with: id) {
                return anchor
            }
        }
        return nil
    }
    private func asAnchor(with identifier: AnyHashable) -> UIView? {
        guard let anchor = self as? ScrollAnchorView.ScrollAnchorBackgroundView, anchor.id == identifier else {
            return nil
        }
        return anchor
    }
}

We can use these methods in our

extension UIView {
    var enclosingScrollView: UIScrollView? {
         sequence(first: self, next: { $0.superview })
            .first(where: { $0 is UIScrollView }) as? UIScrollView
    }
}

7 function. Immediately afterward, we can locate the parent

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4 instance as well:

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

0

This method is called from the

protocol ScrollProxyProtocol {
    /// Scrolls to a child view with the specified identifier.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint)
    /// Scrolls to a child view with the specified identifier and adjusted by the offset position.
    ///
    /// - Parameter identifier: The unique scroll identifier of the child view.
    /// - Parameter anchor: The unit point anchor to describe which edge of the child view
    /// should be snap to.
    /// - Parameter offset: Extra offset on top of the identified view's position.
    func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint)
}

8 implementation within our

struct ScrollReader: View {
    ...
    private final class __ScrollProxy: ScrollProxyProtocol {
        var other: ScrollProxyProtocol? // This is set to the Coordinator's instance
        func scroll(to identifier: AnyHashable, anchor: UnitPoint) {
            other?.scroll(to: identifier, anchor: anchor)
        }
        func scroll(to identifier: AnyHashable, anchor: UnitPoint, offset: CGPoint) {
            other?.scroll(to: identifier, anchor: anchor, offset: offset)
        }
    }
}

0:

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

1

We have overloaded the + operator to add two

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

1 to simplify the syntax in the last step. The following code snippet is used to convert the

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

2’ bounds and the

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

3 anchors into content offset coordinates:

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

2

At this point, we have completed everything for the first iteration of a programmatically scrollable solution:

  • Added a SwiftUI API for our ScrollReader, which in turn is used to publish the proxy object to allow programmatic scrolling
  • Located the target view and its parent ScrollView and converted the positioning of the view to the input parameters that the UIScrollView APIs expect
  • Connected the proxy object with the Coordinator’s concrete implementation

You can find the solution as a Swift project here.

Wrapping up programmatic scrolling

The construction outlined above lets us replicate the behavior of the SwiftUI

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

3 without iOS or SwiftUI version restrictions. Because we have full access to the underlying

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4, we can use this solution to add extra bits of functionality beyond this initial result, something we will explore in subsequent posts.

But even this initial result has an extra bit of functionality: It allows scrolling to a view at an anchor with a local offset applied for more fine-grained control to position our

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5.

Now we can replicate the very first example with our own solution:

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

3

Note that we have replaced the

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

7in the

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

8 function with our own .

struct ScrollAnchorView: UIViewRepresentable {
    let id: AnyHashable
    func makeUIView(context: Context) -> ScrollAnchorBackgroundView {
        let view = ScrollAnchorBackgroundView()
        view.id = id
        return view
    }
    func updateUIView(_ uiView: ScrollAnchorBackgroundView, context: Context) { }
    final class ScrollAnchorBackgroundView: UIView {
        var id: AnyHashable!
    }
}

9 modifier.

And it works as expected:

Error scroll view did scroll is scroll to top swift năm 2024
Figure 4. Scrolling programmatically using our own solution. The first button tap scrolls to the origin coordinate of the first logo image, the second tap scrolls the content 50 points above the top position of the content.

The shortcoming of this iteration is the lack of support for SwiftUI animations. There is no easy way to translate SwiftUI

extension View {
    /// Marks the given view as a potential scroll-to target for programmatic scrolling.
    ///
    /// - Parameter id: An arbitrary unique identifier. Use this id in the scrollview reader's proxy
    /// methods to scroll to this view.
    func scrollAnchor(_ id: AnyHashable) -> some View {
        background(ScrollAnchorView(id: id))
    }
}

0 specifications using the UIKit scroll APIs. We will explore a solution to this problem in a later post.

Conclusion

Building programmatic scrolling in SwiftUI required several iterations to achieve success as we moved through our SwiftUI learning curve. In this current form, however, it is now relatively easy to implement and can be used for simple use cases across the board and even for production features.

But this version is still not the final result. We have managed to take our solution further, adding support for SwiftUI animations, scroll snapping behavior for pagination, and other fine-grained content snapping behavior, and support for adjusting the deceleration rate.

Overall, these steps improved the SwiftUI

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 to allow us to use it for production features while paving the way for our migration to rewrite our consumer app using the SwiftUI framework without compromises.

In even more complex use cases, we have implemented a collaboration between our modal bottom sheet component and a

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 as its content, translating

struct ScrollViewBackgroundReader: UIViewRepresentable {
    let setProxy: (ScrollProxyProtocol) -> ()
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        setProxy(coordinator)
        return coordinator
    }
    func makeUIView(context: Context) -> UIView {
        UIView()
    }
    func updateUIView(_ uiView: UIView, context: Context) { }
}

5 drag gestures into gestures that instead move the sheet itself when appropriate.

One more obvious choice of improvement is fine-turning animation support in this solution. In SwiftUI animations can be specified in a very intuitive and straightforward way, and are more powerful than the choices offered in UIKit: the latter is especially true in the case of

struct ScrollReader<ScrollViewContent: View>: View {
    private let content: (ScrollProxyProtocol) -> Content
    private let proxy = __ScrollProxy()
    init(@ViewBuilder content: @escaping (ScrollProxyProtocol) -> ScrollViewContent) {
        self.content = content
    }
    var body: some View {
        content(proxy)
            .background(
                ScrollViewBackgroundReader(setProxy: { proxy.other = $0 })
            )
    }
}

4 APIs where we can only pick if we want animation or not at all.

In subsequent posts, we will explain adding support for snapping behavior and deceleration rate, as well as how to change this iteration to enable SwiftUI animations. We'll revisit this problem in a later post.

Why is ScrollView not scrolling?

If there is not enough space for scrolling, for example if the ScrollView is placed inside a layout with fixed height, then scrolling will not be possible. Make sure there is enough space for the ScrollView to scroll.nullWhy doesn't my ScrollView scroll? - Physics Forumswww.physicsforums.com › threads › why-doesnt-my-scrollview-scroll.105...null

How to disable ScrollView scroll Swift?

To disable a scrolling, you put . scrollDisabled(true) to the scrollable view, such as List and ScrollView .nullDisable scrolling in SwiftUI ScrollView and List - Sarunwsarunw.com › posts › disable-scrolling-swiftuinull

How to make ScrollView scroll horizontally swift?

If we want to scroll in horizontal direction then we have to add parameter . horizontal in ScrollView. But if we want to scroll in both directions we can mention both like [. vertical, .nullHow to use SwiftUI ScrollView for vertical and horizontal scrollingm.youtube.com › watchnull

How to set scroll view in Swift?

How to Add a Scroll View to the Storyboard in Swift Project (Updated 2023) If the multiplier is not equal to 1, change it to 1: Now select your view and in the size inspector change the top, leading, trailing and bottom constraints to 0: It's done!nullHow to Add a Scroll View to the Storyboard in Swift Project ...medium.com › how-to-add-a-scroll-view-to-the-storyboard-in-swift-project...null