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:
My goal is to have a top bar like the one from Twitter below:
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()
}
}

