Search code examples
swiftuiscrollview

SwiftUI: How do I sync/change progress bar progress based on scrollview’s user current scroll position?


I have horizontal progress bar within ScrollView and I need to change that progress bar value, when user is scrolling.

Is there any way to bind some value to current scroll position?


Solution

  • You can do this with a few GeometryReaders.

    My method:

    1. Get total height of ScrollView container
    2. Get inner height of content
    3. Find the difference for the total scrollable height
    4. Get the distance between the scroll view container top and the content top
    5. Divide the distance between tops by the total scrollable height
    6. Use PreferenceKeys to set the proportion @State value

    Code:

    struct ContentView: View {
        @State private var scrollViewHeight: CGFloat = 0
        @State private var proportion: CGFloat = 0
    
        var body: some View {
            VStack {
                ScrollView {
                    VStack {
                        ForEach(0 ..< 100) { index in
                            Text("Item: \(index + 1)")
                        }
                    }
                    .frame(maxWidth: .infinity)
                    .background(
                        GeometryReader { geo in
                            let scrollLength = geo.size.height - scrollViewHeight
                            let rawProportion = -geo.frame(in: .named("scroll")).minY / scrollLength
                            let proportion = min(max(rawProportion, 0), 1)
    
                            Color.clear
                                .preference(
                                    key: ScrollProportion.self,
                                    value: proportion
                                )
                                .onPreferenceChange(ScrollProportion.self) { proportion in
                                    self.proportion = proportion
                                }
                        }
                    )
                }
                .background(
                    GeometryReader { geo in
                        Color.clear.onAppear {
                            scrollViewHeight = geo.size.height
                        }
                    }
                )
                .coordinateSpace(name: "scroll")
    
                ProgressView(value: proportion, total: 1)
                    .padding(.horizontal)
            }
        }
    }
    
    struct ScrollProportion: PreferenceKey {
        static let defaultValue: CGFloat = 0
    
        static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
            value = nextValue()
        }
    }
    

    Result:

    Result