I would like to achieve attached gif effect in swiftUI, i have tried to make use of several ways to get it to work with no success. When scrolling up the buttons must fade and then once the switcher tab gets to the navbar it should become sticky and the bottom scroll view should continue scrolling. My problem is that i cant seem to get the header to have the correct height set in code hence an extra space is always added.
This is my code:
struct ContentView: View {
var body: some View {
GeometryReader {
let safeArea = $0.safeAreaInsets
let size = $0.size
NavigationView {
Home(safeArea: safeArea, size: size)
.ignoresSafeArea(.container, edges: .top)
}
}
}
}
HomeView:
struct Home: View {
@State private var selectedTab = 1
// MARK: - Properties
var safeArea: EdgeInsets
var size: CGSize
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
// MARK: - Artwork
Artwork()
// Since We ignored Top Edge
GeometryReader{ proxy in
let minY = proxy.frame(in: .named("SCROLL")).minY - safeArea.top
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
tabButton(title: "One", tags: 1)
tabButton(title: "Two", tags: 2)
tabButton(title: "Three", tags: 3)
}
.frame(height: 50)
.background(Color.white)
.font(.headline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.offset(y: minY < 38 ? -(minY - 38) : 0)
}
.frame(height: 50)
.padding(.top, -34)
.zIndex(1)
VStack{
// MARK: - Album View
if selectedTab == 1 {
AlbumView()
} else {
Rates()
}
}
.background(Color(red: 0.949, green: 0.949, blue: 0.949, opacity: 1.0))
.zIndex(0)
}
.background(Color(red: 0.949, green: 0.949, blue: 0.949, opacity: 1.0))
.overlay(alignment: .top) {
HeaderView()
}
}
.background(Color(red: 0.949, green: 0.949, blue: 0.949, opacity: 1.0))
.coordinateSpace(name: "SCROLL")
.navigationTitle("Forex")
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
func Artwork() -> some View {
let height = size.height * 0.45
GeometryReader{ proxy in
let size = proxy.size
let minY = proxy.frame(in: .named("SCROLL")).minY
let progress = minY / (height * (minY > 0 ? 0.5 : 0.8))
Image("background_gradient")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height )
.clipped()
.overlay(content: {
ZStack(alignment: .bottom) {
VStack(spacing: 0) {
Text("View Orders for:")
.foregroundColor(Color.white)
.font(.system(size: 12))
.padding(.top, 8)
HStack(alignment: .center) {
Text("Dunken Miler")
.foregroundColor(Color.white)
.font(.system(size: 27, weight: .semibold))
.padding(.top, 4)
}
Text("Sales for, Mel")
.foregroundColor(Color.white)
.font(.system(size: 16))
.padding(.top, 4)
Spacer()
}
.frame(width: UIScreen.main.bounds.width, height: 180)
.background(
Image("background_gradient")
.resizable()
)
.opacity(1 + (progress > 0 ? -progress : progress))
}
})
.offset(y: -minY)
}
.frame(height: height + safeArea.top )
}
@ViewBuilder
func AlbumView() -> some View {
VStack(spacing: 25) {
ForEach(albums.indices, id: \.self) { index in
HStack(spacing: 25) {
Text("\(index + 1)")
.font(.callout)
.fontWeight(.semibold)
.foregroundColor(.gray)
VStack(alignment: .leading, spacing: 6){
Text(albums[index].albumName)
.fontWeight(.semibold)
.foregroundColor(.black)
Text("2,282,938")
.font(.caption)
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "ellipsis")
.foregroundColor(.gray)
}
}
}
.padding(15)
}
@ViewBuilder
func Rates() -> some View {
VStack(spacing:0) {
ForEach(1...35,id: \.self) { i in
VStack {
HStack {
Text("test")
.font(.body)
.fontWeight(.bold)
.lineLimit(1)
.fixedSize()
Spacer()
Text("test desc")
.font(.body)
.fontWeight(.bold)
.lineLimit(1)
.fixedSize()
}
}
}
}
}
// MARK: - Header View
@ViewBuilder
func HeaderView() -> some View {
GeometryReader{ proxy in
let minY = proxy.frame(in: .named("SCROLL")).minY
let height = size.height * 0.45
let progress = minY / (height * (minY > 0 ? 0.5 : 0.8))
let titleProgress = minY / height
HStack(spacing: 15) {
Button {
} label: {
Image(systemName: "chevron.left")
.font(.title3)
.foregroundColor(.white)
}
Spacer(minLength: 0)
Button {
} label: {
Text("FOLLOWING")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.border(.white, width: 1.5)
}
.opacity(1 + progress)
Button {
} label: {
Image(systemName: "ellipsis")
.font(.title3)
.foregroundColor(.white)
}
}
.background(Color("background_gradient"))
.overlay(content: {
Text("Fally Ipupa")
.fontWeight(.semibold)
.offset(y: -titleProgress > 0.75 ? 0 : 45)
.clipped()
.animation(.easeOut(duration: 0.25), value: -titleProgress > 0.75)
})
.padding(.top, safeArea.top + 10)
.padding([.horizontal,.bottom], 15)
.background(
Color("background_gradient")
.opacity(-progress > 1 ? 1 : 0)
)
.offset(y: -minY)
}
.frame(height: 35)
}
func tabButton(title: String, tags: Int) -> some View {
VStack {
Spacer()
Text("First Tab").foregroundColor(selectedTab == tags ? .red : .gray).font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(selectedTab == tags ? .red : .gray)
Spacer()
Color(selectedTab == tags ? .red : .clear)
.frame(height: 4)
}
.frame(height: 50)
.onTapGesture {
withAnimation { selectedTab = tags }
}
}
}
Dummy data
struct Album: Identifiable{
var id = UUID().uuidString
var albumName: String
}
var albums: [Album] = [
Album(albumName: "Arsenal des belles mélodies"),
Album(albumName: "Bloqué"),
Album(albumName: "Se Yo"),
Album(albumName: "Droit Chemin"),
Album(albumName: "Destin"),
Album(albumName: "Tokooos II"),
Album(albumName: "Tokooos II Gold"),
Album(albumName: "Science - Fiction"),
Album(albumName: "Strandje Aan De Maas"),
Album(albumName: "Inama"),
Album(albumName: "Par Terre - A COLOR SHOW"),
Album(albumName: "QALF infinity"),
Album(albumName: "Berna Reloaded"),
Album(albumName: "Flavour of Africa"),
Album(albumName: "Control"),
Album(albumName: "Gentleman 2.0"),
Album(albumName: "Power 'Kosa Leka' : Vol 1"),
Album(albumName: "Historia"),
Album(albumName: "Tokooos"),
Album(albumName: "Fleur Froide - Second état : la cristalisation"),
]
To ensure the ArtWork view is displayed correctly, a fixed height must be determined for the top-view.
Additionally, a top safe area inset must be calculated and applied to the overlay, so that the content is centered.
The following is the updated ArtWork View implementation:
@ViewBuilder
func Artwork() -> some View {
let height = 220.0
GeometryReader{ proxy in
let size = proxy.size
let minY = proxy.frame(in: .named("SCROLL")).minY
let progress = minY / (height * (minY > 0 ? 0.5 : 0.8))
Image("background_gradient")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height )
.clipped()
.overlay(content: {
ZStack(alignment: .bottom) {
VStack(spacing: 0) {
Text("View Orders for:")
.foregroundColor(Color.white)
.font(.system(size: 12))
.padding(.top, 8)
HStack(alignment: .center) {
Text("Dunken Miler")
.foregroundColor(Color.white)
.font(.system(size: 27, weight: .semibold))
.padding(.top, 4)
}
Text("Sales for, Mel")
.foregroundColor(Color.white)
.font(.system(size: 16))
.padding(.top, 4)
Spacer()
}
.frame(width: UIScreen.main.bounds.width, height: 180)
.background(
Image("background_gradient")
.resizable()
.frame(width: size.width, height: size.height )
)
.opacity(1 + (progress > 0 ? -progress : progress))
}
.offset(y: safeArea.top)
})
.offset(y: -minY)
}
.frame(height: height + safeArea.top )
}