21

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

Candy
  • 397
  • 1
  • 3
  • 6

5 Answers5

16

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 scrool if the screen has stop scrolling

Iker Solozabal
  • 1,232
  • 11
  • 16
Den
  • 3,179
  • 29
  • 26
  • 6
    Unfortunately, applying `DragGesture` to a `ScrollView` will result in the `onChanged` callback only being fired once since scroll views by default handle these events. Thus, this does not produce the correct behaviour. – Darius Mandres Jul 26 '21 at 18:00
  • This appears to work if the ScrollView is at rest initially. If you have already scrolled, and are changing the scroll direction while the scrollview is active this doesn't work. – Donovan Smith Jul 21 '23 at 19:37
  • I have one problem with this solution. It seems that the gesture recogniser competes with the scroll view for consuming the gesture. I have situations where the gesture recogniser is fired but the scroll view does not scroll, or the other way round - the scrolling happens but the recogniser does not get triggered. – wroluk Aug 01 '23 at 10:40
15

You can use DragGesture value

ScrollView {
...
}
.gesture(
   DragGesture().onChanged { value in
      if value.translation.height > 0 {
         print("Scroll down")
      } else {
         print("Scroll up")
      }
   }
)
kimigori
  • 651
  • 6
  • 7
  • 13
    Unfortunately, applying `DragGesture` to a `ScrollView` will result in the `onChanged` callback only being fired once since scroll views by default handle these events. Thus, this does not produce the correct behaviour. – Darius Mandres Jul 26 '21 at 18:00
10

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)
}

}

Marc T.
  • 5,090
  • 1
  • 23
  • 40
  • 5
    This looks promising and gets around the onChange being fired once. How can you use the midY value to detect the direction? Or set a state variable? – Yarm Jul 30 '21 at 07:20
2

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.
Mykel
  • 1,355
  • 15
  • 25
1

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")
          }
        
    })
Erwan
  • 11
  • 2