Search code examples
iosswiftswiftuiscrollview

SwiftUI - Detect when ScrollView has finished scrolling?


I need to find out the exact moment when my ScrollView stops moving. Is that possible with SwiftUI?

Here would be an equivalent for UIScrollView.

I have no idea after thinking a lot about it...

A sample project to test things out:

struct ContentView: View {
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                ForEach(0...100, id: \.self) { i in
                    Rectangle()
                        .frame(width: 200, height: 100)
                        .foregroundColor(.green)
                        .overlay(Text("\(i)"))
                }
            }
            .frame(maxWidth: .infinity)
        }
    }
}

Thanks!


Solution

  • Here is a demo of possible approach - use publisher with changed scrolled content coordinates with debounce, so event reported only after coordinates stopped changing.

    Tested with Xcode 12.1 / iOS 14.1

    UPDATE: verified as worked with Xcode 13.3 / iOS 15.4

    Note: you can play with debounce period to tune it for your needs.

    demo

    import Combine
    
    struct ContentView: View {
        let detector: CurrentValueSubject<CGFloat, Never>
        let publisher: AnyPublisher<CGFloat, Never>
    
        init() {
            let detector = CurrentValueSubject<CGFloat, Never>(0)
            self.publisher = detector
                .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
                .dropFirst()
                .eraseToAnyPublisher()
            self.detector = detector
        }
        
        var body: some View {
            ScrollView {
                VStack(spacing: 20) {
                    ForEach(0...100, id: \.self) { i in
                        Rectangle()
                            .frame(width: 200, height: 100)
                            .foregroundColor(.green)
                            .overlay(Text("\(i)"))
                    }
                }
                .frame(maxWidth: .infinity)
                .background(GeometryReader {
                    Color.clear.preference(key: ViewOffsetKey.self,
                        value: -$0.frame(in: .named("scroll")).origin.y)
                })
                .onPreferenceChange(ViewOffsetKey.self) { detector.send($0) }
            }.coordinateSpace(name: "scroll")
            .onReceive(publisher) {
                print("Stopped on: \($0)")
            }
        }
    }
    
    struct ViewOffsetKey: PreferenceKey {
        typealias Value = CGFloat
        static var defaultValue = CGFloat.zero
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value += nextValue()
        }
    }