Candy
Candy

Reputation: 477

How to detect scroll direction programmatically in SwiftUI ScrollView

I want display or hide items by scroll direction just like safari. Hide something when scroll up, and show it when scroll down.

Upvotes: 28

Views: 33938

Answers (7)

Ilya Biltuev
Ilya Biltuev

Reputation: 450

For iOS 18

You can easily detect scroll direction by using onScrollGeometryChange

ScrollView(.vertical) {
        ...
    }
    .onScrollGeometryChange(for: CGFloat.self, of: { geometry in
        geometry.contentOffset.y
    }, action: { oldValue, newValue in
        if newValue > oldValue {
            print("SCROLL DOWN")
        } else {
            print("SCROLL UP")
        }
    })

Apple doc: https://developer.apple.com/documentation/swiftui/view/onscrollgeometrychange(for:of:action:)

Upvotes: 1

Den
Den

Reputation: 3591

I think, simultaneousGesture is a better solution because it's not blocking scrollView events.

ScrollView {

}
.simultaneousGesture(
       DragGesture().onChanged({
           let isScrollDown = 0 < $0.translation.height
           print(isScrollDown)
       }))

This method only detects a new scroll if the screen has stop scrolling

Upvotes: 23

MarcusWilliams
MarcusWilliams

Reputation: 562

I think @Mykels answer is the best and works well in IOS16. One improvement on it though is to only call the desired functions if the scroll amount is bigger than the minimum offset, otherwise you can end up calling the wrong function if you scroll any amount smaller than the minimum offset. Here is my updated version:

ScrollView(.vertical){
    LazyVStack {
    ...
    }.background(GeometryReader {
        Color.clear.preference(key: ViewOffsetKey.self, value: -$0.frame(in: .named("scroll")).origin.y)
    }).onPreferenceChange(ViewOffsetKey.self) { currentOffset in
         let offsetDifference: CGFloat = self.previousScrollOffset - currentOffset
         if ( abs(offsetDifference) > minimumOffset) {
             if offsetDifference > 0 {
                     print("Is scrolling up toward top.")
              } else {
                      print("Is scrolling down toward bottom.")
              }
              self.previousScrollOffset = currentOffset
         }
    }
}.coordinateSpace(name: "scroll")

struct ViewOffsetKey: PreferenceKey {
        typealias Value = CGFloat
        static var defaultValue = CGFloat.zero
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value += nextValue()
}

Upvotes: 2

Mykel
Mykel

Reputation: 1764

None of the current answers worked for me, so I used PreferenceKey change.

Tested to work in Xcode 14.3.1 and iOS 16.6.

@State var previousViewOffset: CGFloat = 0
let minimumOffset: CGFloat = 16 // Optional

...
ScrollView {
    VStack {
        ...
    }.background(GeometryReader {
        Color.clear.preference(key: ViewOffsetKey.self, value: -$0.frame(in: .named("scroll")).origin.y)
    }).onPreferenceChange(ViewOffsetKey.self) {
        let offsetDifference: CGFloat = abs(self.previousViewOffset - $0)

        if self.previousViewOffset > $0 {
            print("Is scrolling up toward top.")
        } else {
            print("Is scrolling down toward bottom.")
        }

        if offsetDifference > minimumOffset { // This condition is optional but the scroll direction is often too sensitive without a minimum offset.
            self.previousViewOffset = $0
        }
    }
}.coordinateSpace(name: "scroll")
...

struct ViewOffsetKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

To summarize:

  1. You need the background modifier and its contents.
  2. You need the onPreferenceChange modifier and the contents.
  3. You need the coordinateSpace modifier.
  4. You need to ensure the coordinateSpace name matches the named preference frame.
  5. Create a ViewOffsetKey PreferenceKey.

Upvotes: 10

Erwan
Erwan

Reputation: 21

You can use predictedEndLocation and location like this

 /// A prediction, based on the current drag velocity, of where the final
 /// location will be if dragging stopped now.
 public var predictedEndLocation: CGPoint { get }


DragGesture()
        
        .onChanged({ gesture in

          if (gesture.location.y > gesture.predictedEndLocation.y){
            print("up")
          } else {
            print("down")
          }
        
    })

Upvotes: 2

kimigori
kimigori

Reputation: 977

You can use DragGesture value

ScrollView {
...
}
.gesture(
   DragGesture().onChanged { value in
      if value.translation.height > 0 {
         print("Scroll down")
      } else {
         print("Scroll up")
      }
   }
)

Upvotes: 18

Marc T.
Marc T.

Reputation: 5330

You would use GeometryReader to get the global position of one in the views in the ScrollView to detect the scroll direction. The code below will print out the current midY position. Dependent on the +/- value you could display or hide other views.

struct ContentView: View {

var body: some View {
    ScrollView{
        GeometryReader { geometry in

                Text("Top View \(geometry.frame(in: .global).midY)")
                    .frame(width: geometry.size.width, height: 50)
                    .background(Color.orange)
            }

    }.frame(minWidth: 0, idealWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: 0, maxHeight: .infinity, alignment: .center)
}

}

Upvotes: 10

Related Questions