Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Hide/Collapse section on scroll

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.

AirBnB

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

}

}

like image 932
Jason Tremain Avatar asked Aug 14 '19 12:08

Jason Tremain


1 Answers

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()
    }
}
like image 144
Denis Samadov Avatar answered Nov 11 '22 23:11

Denis Samadov