I've been searching for a way to replicate the scroll animation that Airbnb and many other's do with a top portion of the view collapsing/hiding on scroll and reappearing immediately when the user starts scrolling up again. Notice how the "Dates" and "Guest" button fades transparent upon scrolling in the attached image.
Below I've attached a simple view that I just threw together. I've tried including the area I want to collapse both inside and outside of the scrollview. I would guess that it would need to be outside of the scrollview since it would animate independently to the where you are inside of the scroll area.
import SwiftUI
struct HideScrollView: View {
var body: some View {
ScrollView {
HStack {
Text("Hide Me")
Spacer()
}.padding(.horizontal) .frame(height: 60) .background(Color.red) .foregroundColor(Color.white)
ForEach(0 ..< 20) { item in
VStack {
HStack {
Text("Content Items")
Spacer()
}.padding(.horizontal) .frame(height: 40)
}
}
}
}
}
As there is no delegates for scrollView like didScroll or any, there are two ways to achieve the desired result.
#1 You should implement DragGesture gesture, which is not preferred way to do, because gesture won't be called simultaneously.
.simultaneousGesture(DragGesture().onChanged({ transition in
if transition.translation.height > 0{
withAnimation{
self.viewIsShown = true
}
} else {
withAnimation{
self.viewIsShown = false
}
}
})
)
#2 Preferred way: according to Martin's tutorial, you should use GeometryReader
inside ScrollView
as first child, and this giving you possibility to get exact position of element inside GeometryReader
, like this:
ScrollView(.vertical, showsIndicators: false) {
GeometryReader { geometry in
Color.clear.preference(key: OffsetKey.self, value: geometry.frame(in:.global).minY).frame(height: 0)
}
//Your scrollable content here
}
Add your preference key, which conforms to PreferenceKey protocol:
struct OffsetKey: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
Read values which are set in preference key, to do so add .onPreferenceChange(OffsetKey.self)
to ScrollView or any it's parent view.
You should have @State
wrapper for both initialOffest
and offset
.
.onPreferenceChange(OffsetKey.self) {
if self.initialOffset == nil || self.initialOffset == 0 {
self.initialOffset = $0
//setting initialOffest when view first appeared.
}
self.offset = $0
}
Checking difference between initialOffset
and offset
will tell you the scroll state of your view. If initialOffset
is higher than offset
then your view is scrolled down, and you should hide the view you wish.
Accordingly to your code posted in question, it should look like this:
struct HideScrollView: View {
@State var initialOffset: CGFloat?
@State var offset: CGFloat?
@State var viewIsShown: Bool = true
var body: some View {
VStack{
HStack {
Text("Hide Me")
Spacer()
}.padding(.horizontal) .frame(height: 60) .background(Color.red) .foregroundColor(Color.white).opacity(self.viewIsShown ? 1 : 0)
ScrollView {
GeometryReader { geometry in
Color.clear.preference(key: OffsetKey.self, value: geometry.frame(in: .global).minY)
.frame(height: 0)
}
ForEach(0 ..< 20) { item in
VStack {
HStack {
Text("Content Items")
Spacer()
}.padding(.horizontal) .frame(height: 40)
}
}
}
}.onPreferenceChange(OffsetKey.self) {
if self.initialOffset == nil || self.initialOffset == 0 {
self.initialOffset = $0
}
self.offset = $0
guard let initialOffset = self.initialOffset,
let offset = self.offset else {
return
}
if(initialOffset > offset){
self.viewIsShown = false
print("hide")
} else {
self.viewIsShown = true
print("show")
}
}
}
}
struct OffsetKey: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?,
nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With