What is a performant way to change view offset as user scrolls?

I added a background view to my SwiftUI List, and would like to move it up as user scrolls (similar to the effect of that of the Health app). I can't add it onto the background of a List row, because List unconditionally clips content to a row, and I would like the view to extend past a insetted row's bounds (scrollClipDisabled does not work on List). So I added the view as the background view of the entire List.

Currently, I am achieving this by monitoring contentOffset using the new onScrollGeometryChange(for:of:action:) modifier, updating a state variable that controls the offset of the background view. The code looks something like this:


struct ContentView: View {
    @State private var backgroundOffset: CGFloat = 0
    
    var body: some View {
        List {
            ...
        }
        .background {
            backgroundView
                .offset(y: backgroundOffset)
        }
        .onScrollGeometryChange(for: ScrollGeometry.self) { geometry in
            geometry
        } action: { oldValue, newValue in
            let contentOffsetY = newValue.contentOffset.y
            let contentInsetY = newValue.contentInsets.top
            if contentOffsetY <= -contentInsetY {
                backgroundOffset = 0
            } else {
                backgroundOffset = -(contentOffsetY + contentInsetY)
            }
        }
    }
}

However, this results in bad scrolling performance. I am guessing this is due to backgroundOffset being updated too frequently, and thus refreshing the views too often? If so, what is a more performant approach to achieve the desired effect? Thanks!

Answered by DTS Engineer in 798453022

What is a performant way to change view offset as user scrolls?

You'd basically shift the location of a view’s content using offset(x:y:) modifier and update it whenever a user scrolls the view beyond the top of its content. It's a similar logic to what you currently have:


ScrollView {
    // ...
}
.onScrollGeometryChange(for: Bool.self) { geometry in
    geometry.contentOffset.y < geometry.contentInsets.top
} action: { wasBeyondZero, isBeyondZero in
    self.isBeyondZero = isBeyondZero
}

can't add it onto the background of a List row, because List unconditionally clips its content, and I would like the view to extend past the inset grouped list's bounds. So I added the view as the background view of the entire List.

Have you tried using a plain list instead .listStyle(.plain)

What is a performant way to change view offset as user scrolls?

You'd basically shift the location of a view’s content using offset(x:y:) modifier and update it whenever a user scrolls the view beyond the top of its content. It's a similar logic to what you currently have:


ScrollView {
    // ...
}
.onScrollGeometryChange(for: Bool.self) { geometry in
    geometry.contentOffset.y < geometry.contentInsets.top
} action: { wasBeyondZero, isBeyondZero in
    self.isBeyondZero = isBeyondZero
}

can't add it onto the background of a List row, because List unconditionally clips its content, and I would like the view to extend past the inset grouped list's bounds. So I added the view as the background view of the entire List.

Have you tried using a plain list instead .listStyle(.plain)

@DTS Engineer Thanks for the response.

I could use .listStyle(.plain), but I'd still like to maintain the inset grouped style for other rows. I think there is no way to mix-and-match plain and inset grouped styles in the same list?

As I have mentioned, the offset approach seems to cause bad scrolling performance. Any idea why that is?

What is a performant way to change view offset as user scrolls?
 
 
Q