Problem with ScrollView in scroll offset IOS 17

I have a problem with the SwiftUI component in the beta version of iOS 17. In iOS 16, the "parallaxedView" element would stretch its height when scrolling to the top of the screen, and upon releasing the finger, the height would return to its original value. In version 17, it doesn't seem to be working. The "scrollOffset" value attempts to be set, but quickly returns to 0, which prevents the container's height from changing. What could be the issue?

Here's the simplified component code:


private struct ScrollOffsetPreferenceKey: PreferenceKey {
    
    static var defaultValue: CGFloat = .zero
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

struct ParallaxView<ParallaxedView: View, ContentView: View>: View {

    private let parallaxedView: () -> ParallaxedView
    private let contentView: (_ containerHeight: CGFloat) -> ContentView
    private let navBarHeight: CGFloat = 80
    private let maxParallaxedViewHeight: CGFloat = 390
    private let buttonSize: CGFloat = 50

    @State var scrollOffset: CGFloat = 0
    @State var cachedLastOffset: CGFloat = 0
    @State private var containerHeight: CGFloat = 0

    private var parallaxedViewHeight: CGFloat {
        min(UIScreen.main.bounds.size.width, maxParallaxedViewHeight)
    }

    @inlinable
    public init(
        @ViewBuilder
        parallaxedView: @escaping () -> ParallaxedView,
        @ViewBuilder
        contentView: @escaping (_ containerHeight: CGFloat) -> ContentView
    ) {
        self.parallaxedView = parallaxedView
        self.contentView = contentView
    }

    var body: some View {
        GeometryReader { geometry in
            let inset = geometry.safeAreaInsets.top
            let shadowOffset = max(0, min(1, ((scrollOffset) / (parallaxedViewHeight - (inset + navBarHeight)))))
            ZStack(alignment: .topTrailing) {
                ScrollView {
                    ZStack(alignment: .topTrailing) {
                        VStack(spacing: 0) {
                            GeometryReader { geometry in
                                let scrollOffsetInner = -geometry.frame(in: .named("scroll")).minY
                                Color.clear
                                    .preference(
                                        key: ScrollOffsetPreferenceKey.self,
                                        value: scrollOffsetInner
                                    )
                            }
                            .frame(height: 0)
                            .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: { offset in
                                self.scrollOffset = CGFloat(offset)
                            })
                            parallaxedView()
                                .frame(height: parallaxedViewHeight + max(-scrollOffset, 0))
                                .offset(y: scrollOffset / (scrollOffset > 0 ? 2 : 1))
                            contentView(self.containerHeight)
                                .background(Color("Background"))
                                .offset(y: -max(-scrollOffset, 0))
                        }
                        GeometryReader { geometry in
                            Color.clear
                                .onChange(of: geometry.frame(in: .named("scroll")).minY, initial: true) { _, _ in
                                    self.containerHeight = geometry.size.height
                                }
                                .onAppear {
                                    self.containerHeight = geometry.size.height
                                }
                                .frame(minHeight: 0, maxHeight: .infinity)
                                .frame(width: 10)
                                .zIndex(10)
                        }
                    }
                    .padding(.bottom, 15)
                }
                .edgesIgnoringSafeArea(SwiftUI.Edge.Set.top)
                GeometryReader { geometry in
                    VStack(spacing: 0) {
                        Color("Background")
                            .frame(height: geometry.safeAreaInsets.top + navBarHeight - 5)
                        Color("Background")
                            .frame(height: 5)
                            .shadow(color: Color("Black").opacity(0.15), radius: 2, x: 0, y: 4)
                    }

                    .offset(y: -geometry.safeAreaInsets.top)
                    .opacity(shadowOffset)
                }
            }
        }
    }
}

Hey @SebastianKierklo, my team and I are also running into a similar issue for iOS 17. Our code looks somewhat similar to yours under the hood, and we were able to fix it (95%) by updating the content that we are inserting into the scroll view.

My guess is that SwiftUI is trying to optimize some stuff in iOS 17 and depending on the content, can't properly determine the offset. This leads to either the offset resetting or emitting lagging/incorrect values (hence the jitter we are observing).

So a simplified view of what we were adding to our parallax scroll view was something like this:

ParallaxView(....) { // this is content
   LazyVStack {
         LazyVStack {
             itemsSection(with: viewModel.someItemsItems)
         }

          LazyVStack {
             itemsSection(with: viewModel.moreItems)
         }
   }
}


/// down in an extension 
func itemsSection(with items: [...]) -> some View { ForEach(items) {... } }

We fixed it by changing LazyVStacks to VStacks and by making the nested views into their own structs. Changing from lazy to regular stacks may not be optimal, but for us it was okay because 1) we only showed about 20-25 items combined and 2) LazyVStacks by default may not provide proper sizing information. As for making nested subviews into their own types, we did something like this:

private struct ItemsSection: View {
    let items: [..]
    var body: some View {....}
}

The other 5% I mentioned was that after we made the updates, whenever the screen first appears and is rendering the items there is about a 1 second lag of when the screen allows to scroll. I ran it through Profiler and it seems like most of the work is deep in SwiftUI world so I doubt there is much I can do there unless there is an update. But I confirmed that by doing this, it does remove all the jitter completely.

Hope it helps!

Problem with ScrollView in scroll offset IOS 17
 
 
Q