I have a stacked cards view that interact like an iPhone's notifications. Each element is not clickable unless it's fading out (not completely to scale and not fully hidden). Why isn't each rectangle clickable? There is no overlay so I'm not sure why I can't interact with the views.
Copy paste-able code:
import SwiftUI
struct HomeMain2: View {
var body: some View {
StackedCardsNew(items: itemsC, stackedDisplayCount: 1, opacityDisplayCount: 1, itemHeight: 70) { item in
RoundedRectangle(cornerRadius: 25.0)
.foregroundStyle(.blue)
}
.safeAreaPadding(.bottom, 80)
.safeAreaPadding(15)
}
}
struct StackedCardsNew<Content: View, Data: RandomAccessCollection>: View where Data.Element: Identifiable {
var items: Data
var stackedDisplayCount: Int = 2
var opacityDisplayCount: Int = 2
var disablesOpacityEffect: Bool = false
var spacing: CGFloat = 5
var itemHeight: CGFloat
@ViewBuilder var content: (Data.Element) -> Content
var body: some View {
GeometryReader {
let size = $0.size
let topPadding: CGFloat = size.height - itemHeight
ScrollView(.vertical) {
VStack(spacing: spacing) {
ForEach(items) { item in
Button {
//can't click this button
} label: {
content(item)
}
.frame(height: itemHeight)
.visualEffect { content, geometryProxy in
content
.opacity(disablesOpacityEffect ? 1 : opacity(geometryProxy))
.scaleEffect(scale(geometryProxy), anchor: .bottom)
.offset(y: offset(geometryProxy))
}
.zIndex(zIndex(item))
}
}
.scrollTargetLayout()
}
.scrollIndicators(.hidden)
.safeAreaPadding(.top, topPadding)
}
}
func headerOffset(_ proxy: GeometryProxy, _ topPadding: CGFloat) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let viewSize = proxy.size.height - itemHeight
return -minY > (topPadding - viewSize) ? -viewSize : -minY - topPadding
}
func zIndex(_ item: Data.Element) -> Double {
if let index = items.firstIndex(where: { $0.id == item.id }) as? Int {
return Double(items.count) - Double(index)
}
return 0
}
func offset(_ proxy: GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let progress = minY / itemHeight
let maxOffset = CGFloat(stackedDisplayCount) * offsetForEachItem
let offset = max(min(progress * offsetForEachItem, maxOffset), 0)
return minY < 0 ? 0 : -minY + offset
}
func scale(_ proxy: GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let progress = minY / itemHeight
let maxScale = CGFloat(stackedDisplayCount) * scaleForEachItem
let scale = max(min(progress * scaleForEachItem, maxScale), 0)
return 1 - scale
}
func opacity(_ proxy: GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let progress = minY / itemHeight
let opacityForItem = 1 / CGFloat(opacityDisplayCount + 1)
let maxOpacity = CGFloat(opacityForItem) * CGFloat(opacityDisplayCount + 1)
let opacity = max(min(progress * opacityForItem, maxOpacity), 0)
return progress < CGFloat(opacityDisplayCount + 1) ? 1 - opacity : 0
}
var offsetForEachItem: CGFloat { return 8 }
var scaleForEachItem: CGFloat { return 0.08 }
}
// IGNORE
struct ItemC: Identifiable{
var id: UUID = .init()
var logo: String
var title: String
var description: String = "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
}
var itemsC: [ItemC] = [
ItemC(logo: "", title: ""),
ItemC(logo: "Amazon", title: "Amazon"),
ItemC(logo: "Youtube", title: "Youtube"),
ItemC(logo: "Dribbble", title: "Dribbble"),
ItemC(logo: "Apple", title: "Apple"),
ItemC(logo: "Patreon", title: "Patreon"),
ItemC(logo: "Instagram", title: "Instagram"),
ItemC(logo: "Netflix", title: "Netflix"),
ItemC(logo: "Photoshop", title: "Photoshop"),
ItemC(logo: "Figma", title: "Figma")
]
#Preview {
HomeMain2()
}
I think the issue is caused by .safeAreaPadding(.top, topPadding)
and .offset(y: offset(geometryProxy))
. It seems that all taps above topPadding
are ignored.
So, the workaround is to put your buttons lower in ScrollView by adding topPadding to offset(geometryProxy): .offset(y: offset(geometryProxy) + topPadding)
. Also I added Color.clear.frame(height: topPadding)
to give scroll view some space for scrolling.
Here is the full code updated:
import SwiftUI
struct HomeMain2: View {
var body: some View {
StackedCardsNew(items: itemsC, stackedDisplayCount: 1, opacityDisplayCount: 1, itemHeight: 70) { item in
RoundedRectangle(cornerRadius: 25.0)
.foregroundStyle(.blue)
}
.safeAreaPadding(.bottom, 80)
.safeAreaPadding(15)
}
}
struct StackedCardsNew<Content: View, Data: RandomAccessCollection>: View where Data.Element: Identifiable {
var items: Data
var stackedDisplayCount: Int = 2
var opacityDisplayCount: Int = 2
var disablesOpacityEffect: Bool = false
var spacing: CGFloat = 5
var itemHeight: CGFloat
@ViewBuilder var content: (Data.Element) -> Content
var body: some View {
GeometryReader {
let size = $0.size
let topPadding: CGFloat = size.height - itemHeight
ScrollView(.vertical) {
VStack(spacing: spacing) {
ForEach(items) { item in
Button {
// now you can click
} label: {
content(item)
}
.frame(height: itemHeight)
.visualEffect { content, geometryProxy in
content
.opacity(disablesOpacityEffect ? 1 : opacity(geometryProxy))
.scaleEffect(scale(geometryProxy), anchor: .bottom)
// Added "+ topPadding" to get the same screen position of the elements
.offset(y: offset(geometryProxy) + topPadding)
}
.zIndex(zIndex(item))
}
}
.scrollTargetLayout()
// Added some placeholder for corrrect scroll behavior
Color.clear
.frame(height: topPadding)
}
.scrollIndicators(.hidden)
}
}
// Nothing is changed below this line
func headerOffset(_ proxy: GeometryProxy, _ topPadding: CGFloat) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let viewSize = proxy.size.height - itemHeight
return -minY > (topPadding - viewSize) ? -viewSize : -minY - topPadding
}
func zIndex(_ item: Data.Element) -> Double {
if let index = items.firstIndex(where: { $0.id == item.id }) as? Int {
return Double(items.count) - Double(index)
}
return 0
}
func offset(_ proxy: GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let progress = minY / itemHeight
let maxOffset = CGFloat(stackedDisplayCount) * offsetForEachItem
let offset = max(min(progress * offsetForEachItem, maxOffset), 0)
return minY < 0 ? 0 : -minY + offset
}
func scale(_ proxy: GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let progress = minY / itemHeight
let maxScale = CGFloat(stackedDisplayCount) * scaleForEachItem
let scale = max(min(progress * scaleForEachItem, maxScale), 0)
return 1 - scale
}
func opacity(_ proxy: GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView(axis: .vertical)).minY
let progress = minY / itemHeight
let opacityForItem = 1 / CGFloat(opacityDisplayCount + 1)
let maxOpacity = CGFloat(opacityForItem) * CGFloat(opacityDisplayCount + 1)
let opacity = max(min(progress * opacityForItem, maxOpacity), 0)
return progress < CGFloat(opacityDisplayCount + 1) ? 1 - opacity : 0
}
var offsetForEachItem: CGFloat { return 8 }
var scaleForEachItem: CGFloat { return 0.08 }
}
// IGNORE
struct ItemC: Identifiable{
var id: UUID = .init()
var logo: String
var title: String
var description: String = "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
}
var itemsC: [ItemC] = [
ItemC(logo: "", title: ""),
ItemC(logo: "Amazon", title: "Amazon"),
ItemC(logo: "Youtube", title: "Youtube"),
ItemC(logo: "Dribbble", title: "Dribbble"),
ItemC(logo: "Apple", title: "Apple"),
ItemC(logo: "Patreon", title: "Patreon"),
ItemC(logo: "Instagram", title: "Instagram"),
ItemC(logo: "Netflix", title: "Netflix"),
ItemC(logo: "Photoshop", title: "Photoshop"),
ItemC(logo: "Figma", title: "Figma")
]
#Preview {
HomeMain2()
}