SwiftUI: Observing scroll position via View.scrollPosition(id:anchor:) breaks scrolling

638 views Asked by At

I'm trying to use the new (announced at WWDC 2023 and available in iOS 17 and macOS 14) API to observe scroll position of SwiftUI ScrollView.

I have the following view, but I can't make it work correctly. That is, using the scrollPosition(id:anchor) view modifier makes the scroll view go nuts. At some point, and in some cases even at the very begging, the scroll view just starts jumping and is stuttering when trying to scroll. See the screen recording here: https://twitter.com/krajaac/status/1714191037927764149

The code:

ScrollView(.vertical) {
                LazyVGrid(
                    columns: [.init(.adaptive(minimum: self.itemSize.width), spacing: Constants.columnSpacing, alignment: .center)],
                    alignment: .center,
                    spacing: Constants.sectionSpacing
                ) {
                    ForEach(self.sectionData) { section in
                        Section(section.title) {
                            ForEach(section.items) { item in
                                Text(item.text)
                                    .frame(width: self.itemSize.width, height: self.itemSize.height)
                            }
                        }
                    }
                }
                .scrollTargetLayout()
            }
            .scrollPosition(id: self.$currentItemID) // TODO: Comment out this line to make the scrolling work

The whole code can be found in my GitHub repo: https://github.com/tomaskraina/feedbackassistant.apple.com/blob/master/ScrollPositionVsLazyVGrid/ScrollPositionVsLazyVGrid/ContentView.swift

Am I doing something wrong or is this a bug in SwiftUI?

1

There are 1 answers

0
Tom Kraina On

I still haven't figured out the root cause of this, but "hiding" the content of the ScrollView -- LazyVGrid in a custom View like this fixes the issues with scrolling for some reason:

struct ContentView: View {

    private var itemSize: CGSize = .init(width: 50.0, height: 50.0)

    // Annotated as @State to create the sections only once
    @State private var sectionData: [SectionInfo] = makeSections()

    // Tracking scroll position
    @State private var currentItemID: MyIdentifier?
    var currentItemDescription: String { self.currentItemID?.stringId ?? "nil" }

    var body: some View {
        VStack {
            // 1. Scroll View
            ScrollView(.vertical) {
                // ContentGrid is just a custom view with LazyVGrid underneath
                ContentGrid(itemSize: self.itemSize, sectionData: self.sectionData)
                .scrollTargetLayout()
            }
            .scrollPosition(id: self.$currentItemID)

            Divider()

            // 2. Footer
            Group {
                Label(
                    title: { Text(self.currentItemDescription) },
                    icon: { Image(systemName: "42.circle") }
                )
            }
            .padding()
        }
        #if os(macOS)
        .frame(width: 320, height: 480, alignment: .center)
        #endif
    }
}

// MARK: - Private

struct ContentGrid: View {

    var itemSize: CGSize
    var sectionData: [SectionInfo]

    var body: some View {
        LazyVGrid(
            columns: [.init(.adaptive(minimum: self.itemSize.width), spacing: Constants.columnSpacing, alignment: .center)],
            alignment: .center,
            spacing: Constants.sectionSpacing
        ) {
            ForEach(self.sectionData) { section in
                Section(section.title) {
                    ForEach(section.items) { item in
                        Text(item.text)
                            .frame(width: self.itemSize.width, height: self.itemSize.height)
                    }
                }
            }
        }
    }
}

See the screen recording here: https://mastodon.social/@tomkraina/111251021544613267