Search code examples
iosswiftswiftuiuikitswiftui-scrollview

How to achieve the fade plus sticky header screen effect in SwiftUI


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"),
    
]

Desired effect gif


Solution

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

    Preview