TruMan1
TruMan1

Reputation: 36098

How to make Button in background tappable with ScrollView on top?

I have a ScrollView that slides over the header, but I need a button in the header tappable.

This is what it looks like:

enter image description here

I need the "Start" button tappable, but because the ScrollView is on top, it's not tappable. I tried playing with the zIndex but the scroll no longer occurred on top of the header.

This is the sample code:

struct ContentView: View {
    @State private var isPresented = false
    @State private var headerHeight: CGFloat = 0

    var body: some View {
        ZStack(alignment: .top) {
            VStack {
                Button("Start") {
                    isPresented = true
                }
                .padding()
                .frame(maxWidth: .infinity)
                .foregroundColor(.white)
                .background(Color.blue)
                .cornerRadius(16)
            }
            .padding(80)
            .background(Color(.label).ignoresSafeArea())
            .assign(heightTo: $headerHeight)
            ScrollView(showsIndicators: false) {
                Spacer()
                    .frame(height: headerHeight)
                VStack {
                    ForEach((0...50), id: \.self) {
                        Text("Some text \($0)")
                    }
                }
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color(.white))
            }
        }
        .navigationBarHidden(true)
        .alert(isPresented: $isPresented) {
            Alert(title: Text("Button tapped"))
        }
    }
}

extension View {
    /// Binds the height of the view to a property.
    func assign(heightTo height: Binding<CGFloat>) -> some View {
        background(
            GeometryReader { geometry in
                Color.clear.onAppear {
                    height.wrappedValue = geometry.size.height
                }
            }
        )
    }
}

How can this be achieved while making the scroll view slide over the header?

Upvotes: 5

Views: 1860

Answers (4)

Andr&#233; Neves
Andr&#233; Neves

Reputation: 1076

I ended up following the approach below which I find super clean and does exactly what's needed. (iOS 14+ though)

ScrollView {
    LazyVStack(pinnedViews: .sectionHeaders) {
        Section {
            ScrollableContentView()
                .zIndex(0)
        } header: {
            HeaderViewToHideUnderneath()
                .zIndex(-1)
        }
    }
}  

Upvotes: 0

swiftPunk
swiftPunk

Reputation: 1

Version 1.0.0


It was not an easy Job! But I make it working with down way:

The way that I used was watching ScrollView move and using that data for app of course with power of Almighty GeometryReader!


enter image description here


import SwiftUI

struct ContentView: View {

    @State private var isPresented: Bool = Bool()
    @State private var offsetY: CGFloat = CGFloat()
    @State private var headerHeight: CGFloat = CGFloat()

    var body: some View {
 
        GeometryReader { screenGeometry in

            ZStack {
                
                Color.yellow.ignoresSafeArea()

                ScrollView(showsIndicators: false) {
  
                    VStack(spacing: 0.0) {

                        Color.clear                         // Test if we are in Center! >> change Color.clear to Color.blue and uncomment down code for Capsule() to see it!
                            .frame(height: headerHeight)
                            //.overlay(Capsule().frame(height: 2.0))
                            .overlay( ButtonView(isPresented: $isPresented)
                                        .background( GeometryReader { proxy in Color.clear.onAppear { headerHeight = proxy.size.height } } )
                                        .offset(y: headerHeight + screenGeometry.safeAreaInsets.top - offsetY))

                        ZStack {

                            Color.white.cornerRadius(20.0)

                            VStack { ForEach((0...30), id: \.self) { item in Text("item " + item.description); Divider().padding(.horizontal) } }.padding(.top)
   
                        }
                        .padding(.horizontal)
                        .overlay( GeometryReader { proxy in Color.clear.onChange(of: proxy.frame(in: .global).minY) { newValue in offsetY = newValue } } )
 
                    }

                }

            }
            .position(x: screenGeometry.size.width/2, y: screenGeometry.size.height/2)
            .alert(isPresented: $isPresented) { Alert(title: Text("Button tapped")) }
            .statusBar(hidden: true)

        }
        .shadow(radius: 10.0)

    }

}

struct ButtonView: View {

    @Binding var isPresented: Bool

    var body: some View {

        Button(action: { isPresented.toggle() }, label: {
            
            Text("Start")
                .bold()
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.purple)
                .foregroundColor(.white)
                .cornerRadius(16)
            
        })
        .padding(80)
  
    }
 
}

Upvotes: 4

swiftPunk
swiftPunk

Reputation: 1

Version 2.0.0


I decided to updated my answer and because it can be confused if i put them in same answer, I am putting it here, to keep codes and answers much clear and easy to read. about new update, it is basically the same way but a souped-up version!


enter image description here


import SwiftUI

struct ContentView: View {
    
    @State private var isPresented: Bool = Bool()
    @State private var offsetY: CGFloat = CGFloat()
    @State private var headerHeight: CGFloat = CGFloat()
    
    var body: some View {
        
        GeometryReader { screenGeometry in
            
            ZStack {
                
                Color.yellow.ignoresSafeArea()
                
                ScrollView(showsIndicators: false) {
                    
                    VStack(spacing: 0.0) {
                        
                        Color.clear                         // Test if we are in Center! >> change Color.clear to Color.blue and uncomment down code for Capsule() to see it!
                            .frame(height: headerHeight)
                            //.overlay(Capsule().frame(height: 2.0))
                            .overlay( HeaderView(isPresented: $isPresented)
                                        .background( GeometryReader { proxy in Color.clear.onAppear { headerHeight = proxy.size.height } } )
                                        .offset(y: headerHeight + screenGeometry.safeAreaInsets.top - offsetY))
                        
                        ZStack {
                            
                            Color.white.cornerRadius(20.0)
                            
                            VStack { ForEach((0...30), id: \.self) { item in Text("item " + item.description); Divider().padding(.horizontal) } }.padding(.top)
                            
                        }
                        .padding(.horizontal)
                        .overlay( GeometryReader { proxy in Color.clear.onChange(of: proxy.frame(in: .global).minY) { newValue in offsetY = newValue } } )
                        
                    }
                    
                }
                
            }
            .position(x: screenGeometry.size.width/2, y: screenGeometry.size.height/2)
            .alert(isPresented: $isPresented) { Alert(title: Text("Button tapped")) }
            .statusBar(hidden: true)
            
        }
        .shadow(radius: 10.0)
        
    }
    
}

struct HeaderView: View {
    
    @Binding var isPresented: Bool
    
    var body: some View {
        
        ZStack {
            
            Color.clear
            
            VStack(spacing: 20.0) {
                
                button(color: Color(UIColor.systemTeal))
                
                Text("Some text 1").bold()
                
                Text("Some text 2").bold()
                
                button(color: Color(UIColor.green))
                
                Text("Some text 3").bold()
                
                Text("Some text 4").bold()
                
                button(color: Color.purple)
                
            }
            
        }
        .padding()
        
    }
    
    func button(color: Color) -> some View {
        
        return Button(action: { isPresented.toggle() }, label: {
            
            Text("Start")
                .bold()
                .padding()
                .shadow(radius: 10.0)
                .frame(maxWidth: .infinity)
                .background(color)
                .foregroundColor(.white)
                .cornerRadius(16)
            
        })
        
    }
    
}

Upvotes: 5

TruMan1
TruMan1

Reputation: 36098

Inspired by @swiftPunk's answer, I used a slightly different approach by locking the header offset to the initial diff off the scroll position:

struct ContentView3: View {
    @State private var isPresented = false

    // For sticky header
    @State private var scrollOffsetY: CGFloat = 0
    @State private var headerOffsetY: CGFloat = 0
    @State private var headerOffsetDiff: CGFloat = 0
    @State private var headerHeight: CGFloat = 0

    var body: some View {
        ScrollView {
            Color.clear
                .frame(height: headerHeight)
                .overlay(
                    VStack {
                        Text("Some text 1")
                        Text("Some text 1")
                        Text("Some text 1")
                        Text("Some text 1")
                        Text("Some text 1")
                        Button(action: { isPresented.toggle() }) {
                            Text("Start")
                                .padding()
                                .frame(maxWidth: .infinity)
                                .foregroundColor(.white)
                                .background(Color.blue)
                                .cornerRadius(16)

                        }
                    }
                    .foregroundColor(.red)
                    .padding()
                    .background(
                        GeometryReader { geometry in
                            Color.clear
                                .onAppear {
                                    headerHeight = geometry.size.height
                                    headerOffsetY = geometry.frame(in: .global).minY
                                }
                        }
                    )
                    .onChange(of: scrollOffsetY) { offset in
                        guard headerOffsetDiff == 0 else { return }
                        headerOffsetDiff = offset - headerOffsetY
                    }
                    .offset(y: headerOffsetY + headerOffsetDiff - scrollOffsetY)
                )
            VStack {
                ForEach((0...30), id: \.self) {
                    Text("Some text \($0)").padding(.bottom)
                }
            }
            .frame(maxWidth: .infinity)
            .padding()
            .background(
                GeometryReader { geometry in
                    Color(.systemBackground)
                        .onChange(of: geometry.frame(in: .global).minY) {
                            scrollOffsetY = $0
                        }
                }
            )
        }
        .background(
            Color(.label)
                .ignoresSafeArea()
        )
        .alert(isPresented: $isPresented) {
            Alert(title: Text("Button tapped"))
        }
    }
}

Upvotes: 0

Related Questions