Reputation: 36098
I have a ScrollView
that slides over the header, but I need a button in the header tappable.
This is what it looks like:
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
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
Reputation: 1
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!
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
Reputation: 1
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!
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
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