Collapsing Top Bar in SwiftUI

Viewed 364

I have spent that past few days looking for a collapsing top bar in SwiftUI. I currently have a solution that I made from this article. I have adjusted the original code slightly, but I still encounter a bug when I scroll to the bottom of the ScrollView:

enter image description here

My goal is to have a top bar like the one from Twitter below:

enter image description here

While I may be able to find a solution to the bug, I was hoping to find a more stable and customizable top bar in SwiftUI. I have looked for libraries and other related solutions to making a custom top bar, but I have not found anything that good.

Here is my code:

ContentView.swift

struct ContentView: View {
    @State var initialOffset: CGFloat?
    @State var offset: CGFloat?
    @State var previousOffset: CGFloat?
    
    var isOffset : Bool {
        // previousOffset! < offset
        if TopBar.heightAndRadiusForBackground(initialOffset: self.initialOffset, offset: self.offset, previousOffset: self.previousOffset) != 160.0 {
            return true
        }
        else {
            return false
        }
    }
    
    var body: some View {
        VStack {
            TopBar(offset: self.$offset,
                   initialOffset: self.$initialOffset,
                   previousOffset: self.$previousOffset)
            
            ScrollView {
                GeometryReader { geometry in
                    Color.clear
                        .preference(key: OffsetKey.self,
                                    value: geometry.frame(in: .global).minY
                    )
                        .frame(height: 0)
                }
                LazyVStack {
                    ForEach((1...10), id: \.self) {
                        Text("\($0)")
                            .font(.title)
                            .bold()
                            .frame(width: UIScreen.main.bounds.width, height: 200, alignment: .center)
                    }
                }
            }
            .padding(.top, self.isOffset ? -16 : 40)
            .animation(.easeOut)
        }

        .onPreferenceChange(OffsetKey.self) {
            if self.initialOffset == nil || self.initialOffset == 0 {
                self.initialOffset = $0
            }
            if self.offset != nil {
                self.previousOffset = self.offset
            }
            self.offset = $0
        }
    }
}

TopBar.swift

struct TopBar: View {
    @Binding var offset: CGFloat?
    @Binding var initialOffset: CGFloat?
    @Binding var previousOffset: CGFloat?
    
    var isOffset : Bool {
        if TopBar.heightAndRadiusForBackground(initialOffset: self.initialOffset, offset: self.offset, previousOffset: self.previousOffset) != 160.0 {
            return true
        }
        else {
            return false
        }
    }
    
    func viewForBackground() -> some View {
        let newHeight = TopBar.heightAndRadiusForBackground(initialOffset: self.initialOffset, offset: self.offset, previousOffset: self.previousOffset)
        return Rectangle()
            .foregroundColor(.green)
            .frame(width: UIScreen.main.bounds.width, height: newHeight)
            .offset(y: self.isOffset ? -32 : -12)
    }
    
    var body: some View {
        HStack {
            Text("Top Bar")
                .font(.system(size: 34, weight: .bold, design: .default))
            Spacer()
        }
        .offset(y: self.isOffset ? -16 : UIScreen.main.bounds.height/28)
        .padding(.leading)
        .animation(.easeInOut)
        .background(viewForBackground().animation(.easeOut))
    }
}

extension TopBar {
    static func heightAndRadiusForBackground(initialOffset: CGFloat?, offset: CGFloat?, previousOffset: CGFloat?) -> CGFloat {
        let maxHeight: CGFloat = 160.0
        let minHeight: CGFloat = 90
        
        guard let initialOffset = initialOffset,
              let offset = offset else { return maxHeight }
        
        if previousOffset == nil {
            return maxHeight
        }
        
        // Previous < offset: Scrolling down
        // Previous > offset: Scrolliing up
        if initialOffset > offset {
            if previousOffset! < offset {
                return maxHeight
            }
            else {
                return minHeight
            }
        }
        
        return maxHeight
    }
}


struct OffsetKey: PreferenceKey {
    static let defaultValue: CGFloat? = nil
    static func reduce(value: inout CGFloat?,
                       nextValue: () -> CGFloat?) {
        value = value ?? nextValue()
    }
}

Project

0 Answers
Related