29

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

2
  • 1
    You can potentially calculate this using .onAppear() when your scrolled content appears. Commented Dec 15, 2019 at 9:27
  • 1
    onAppear? in could be only once when you opening View, he asks about scroll view was SCROLL Commented May 9, 2024 at 14:18

7 Answers 7

26

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

Sign up to request clarification or add additional context in comments.

3 Comments

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.
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.
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.
19

You can use DragGesture value

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

1 Comment

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.
11

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

}

1 Comment

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?
10

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.

Comments

6

For iOS 17

struct ContentView: View {
    @State private var scrollOffset: CGPoint = .zero

    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<50) { i in
                    Text("Row \(i)")
                        .frame(height: 30)
                        .id(i)
                }
            }
            .background(GeometryReader { geometry in
                Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
            })
            .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
                // Detect direction based on changes in value.x and value.y
                if value.y < scrollOffset.y {
                    print("Scrolling Up")
                } else if value.y > scrollOffset.y {
                    print("Scrolling Down")
                }
                scrollOffset = value
            }
            .coordinateSpace(name: "scroll")
        }
    }
}

struct ScrollOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGPoint = .zero

    static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
        value = nextValue()
    }
}

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

2 Comments

how many users use iOS 18?
according to Apple statistics on their official site on January 21, 2025: 68% of all devices use iOS 18. developer.apple.com/support/app-store
3

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

Comments

2

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.