I want display or hide items by scroll direction just like safari. Hide something when scroll up, and show it when scroll down.
7 Answers
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
3 Comments
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.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
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.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
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:
- You need the
backgroundmodifier and its contents. - You need the
onPreferenceChangemodifier and the contents. - You need the
coordinateSpacemodifier. - You need to ensure the
coordinateSpacename matches thenamedpreference frame. - Create a
ViewOffsetKeyPreferenceKey.
Comments
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
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
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")
}
})
.onAppear()when your scrolled content appears.