onTapGesture not working when view is inside ScrollView? - ios

I have simple view.
var body: some View {
NavigationView {
ZStack {
ScrollView {
GeometryReader { geometry in
CardView(geometry: geometry)
.onTapGesture {
print("Tapped")
}
}
.padding()
}
}
}
When I m tapping on the card, nothing is getting printed. However if I change scrollView to VStack for instance, I instantly get Tapped on the console. What is happening? How can I implement tap gesture on my cards which are inside scrollView?

Your problem is probably on the GeometryReader, try to move it above the scrollView instead of inside
var body: some View {
NavigationView {
ZStack {
Color("light_blue_grey")
.edgesIgnoringSafeArea(.all)
GeometryReader { geometry in
ScrollView {
Rectangle()
.foregroundColor(.blue)
.frame(width: geometry.size.width, height: 100)
.onTapGesture {
print("Tapped!")
}
}
.padding()
}
}
}
}

Related

SwiftUI onTapGuesture not working using offset inside ZStack?

I have a Mapbox map view and a search bar and activate button (HStack) inside a ZStack and using an offset modifier to position them at the top of the screen.
For some reason the offset is preventing the onTapGesture from working for the activate button, if I comment out the offset it will work but will be placed at the bottom of the screen.
I tried adding the offset to each element individually inside the HStack but that did not work...
How can I make the onTapGesture functionality work with offset?
Thank you
struct MapScreen: View {
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
HStack(spacing: 10) {
SearchBar()
.padding(.leading, 5)
.onTapGesture {
print("search pressed")
}
ActivateButton()
.onTapGesture {
print("activate pressed")
}
}.zIndex(2)
.offset(y: -770)
MapView(locations: $locations)
.edgesIgnoringSafeArea([.top, .bottom])
BottomNavBar().zIndex(1)
.edgesIgnoringSafeArea(.all).offset(y: 35)
}
.navigationViewStyle(StackNavigationViewStyle())
.fullScreenCover(isPresented: $presentSearchView, content: {
SearchView()
})
}
}
}
There's a couple problems here:
.offset(y: -770)
If you're trying to use an offset so large, you shouldn't be using offset at all. offset is usually for fine-tune adjustments and doesn't work great with big values. And also, 770 is hardcoded. What happens when you use another device with a different screen size? Don't hardcode or do calculations yourself — SwiftUI can do it for you!
Instead, use a VStack + Spacer() to push the search bar up.
ZStack(alignment: .bottom) {
VStack { /// here!
HStack(spacing: 10) {
SearchBar()
.padding(.leading, 5)
.onTapGesture {
print("search pressed")
}
ActivateButton()
.onTapGesture {
print("activate pressed")
}
}
Spacer() /// push the `HStack` to the top of the screen
}
.zIndex(2)
MapView(locations: $locations)
.edgesIgnoringSafeArea([.top, .bottom])
BottomNavBar().zIndex(1)
.edgesIgnoringSafeArea(.all).offset(y: 35)
}

How to set content to Top in ScrollView?

I am trying to set the content Text("Test.....") inside the scroll view to the top. Without using .frame (maxHeight: 150) and the like. I just need to kick it up under Text ("123")) but nothing comes out. And while scrolling in all directions should work. This is just an example to understand how to do it technically. Guys who can help!
import SwiftUI
struct MainView: View {
var body: some View {
NavigationView {
content
}
.padding()
}
}
private extension MainView {
var content: some View {
VStack {
ZStack(alignment: .topLeading) {
ScrollView([.vertical, .horizontal]) {
Text("TestTestTestTestTestTestTestTestTestTestTest")// set to top in content
.frame(height: 100)
.background(Color.green)
//.position(y: 0)
}
Text("123")
}
.background(Color.blue)
}
}
}
You can use .offset() for that
.offset(y: -230)
or
.offset(y: UIScreen.main.bounds.height * -0.28) // for responsive

Possible to allowsHitTesting on part of view?

I have a button behind a ScrollView, but cannot tap it since it's under it. I have a Spacer at the top of the scroll view that shows the button. I tried putting allowsHitTesting(false) on the Spacer, but this is still not letting it pass underneath the ScrollView.
This is what the `ScrollView looks like, I cannot tap the "Press" button:
This is the code, notice the button is in the ZStack, and the Spacer in the ScrollView has the allowsHitTesting(false):
struct ContentView: View {
#State private var isPresented = false
var body: some View {
ZStack(alignment: .top) {
Button("Press") {
isPresented = true
}
.frame(maxWidth: .infinity, maxHeight: 200)
.background(Color(.label).ignoresSafeArea())
ScrollView {
Spacer()
.frame(height: 200)
.allowsHitTesting(false) // <---- Will not tap thru ScrollView!!
VStack {
Button("Another Press") {
isPresented = true
}
.padding(50)
ForEach((0...50), id: \.self) {
Text("Some text \($0)")
}
}
.frame(maxWidth: .infinity)
.background(Color(.white))
}
}
.navigationBarHidden(true)
.alert(isPresented: $isPresented) {
Alert(title: Text("Button tapped"))
}
}
}
Tapping the button in the background under the Spacer doesn't work. Is there a way to apply the allowsHitTesting(false) to part of the ScrollView that the Spacer occupies? I obviously don't want to apply allowsHitTesting(false) on the entire ScrollView because this is a simplistic example but a real app would have tons of interaction views within the ScrollView. Thanks for any help or insight!

Custom ScrollView Indicator in SwiftUI

Is it possible to create a custom horizontal indicator that has empty and filled circles to show how many images there are and the current position?
The below attempt uses a lazyHStack and OnAppear but, judging from the console output, it doesn't work properly since scrolling back and forth doesn't recall the onAppear consistently.
import SwiftUI
struct ContentView: View {
let horizontalScrollItems = ["wind", "hare.fill", "tortoise.fill", "rosette" ]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(horizontalScrollItems, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 200))
.frame(width: geometry.size.width)
.onAppear(){print("\(symbol)")}
}
}
}
}
}
}
This is the desired indicator. I'm just not sure how to properly fill and empty each circle as the user scrolls back and forth. Appreciate the help!
You can get the desired result using TabView() and PageTabViewStyle()
Note : This will work from SwiftUI 2.0
Here is the code :
struct ContentView: View {
let horizontalScrollItems = ["wind", "hare.fill", "tortoise.fill", "rosette" ]
var body: some View {
GeometryReader { geometry in
TabView(){
ForEach(horizontalScrollItems, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 200))
.frame(width: geometry.size.width)
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
Result :

Tap Action not working when Color is clear SwiftUI

my tapAction is not recognizing a tap when my foregroundColor is clear. When i remove the color it works fine.
That's my code:
ZStack {
RoundedRectangle(cornerRadius: 0)
.foregroundColor(Color.clear)
.frame(width: showMenu ? UIScreen.main.bounds.width : 0)
.tapAction {
self.showMenu.toggle()
}
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.green)
.shadow(radius: 5, y: 2)
.padding(.trailing, 50)
.frame(width: showMenu ? UIScreen.main.bounds.width : 0)
}
.edgesIgnoringSafeArea(.top)
The accurate way is to use .contentShape(Rectangle()) on the view.
Described in this tutorial:
control-the-tappable-area-of-a-view by Paul Hudson #twostraws
VStack {
Image("Some Image").resizable().frame(width: 50, height: 50)
Spacer().frame(height: 50)
Text("Some Text")
}
.contentShape(Rectangle())
.onTapGesture {
print("Do Something")
}
how-to-control-the-tappable-area-of-a-view-using-contentshape stackoverflow
I have also discovered that a shape filled with Color.clear does not generate a tappable area.
Here are two workarounds:
Use Color.black.opacity(0.0001) (even on 10-bits-per-channel displays). This generates a color that is so transparent that it should have no effect on your appearance, and generates a tappable area that fills its frame. I don't know if SwiftUI is smart enough to skip rendering the color, so I don't know if it has any performance impact.
Use a GeometryReader to get the frame size, and then use the contentShape to generate the tappable area:
GeometryReader { proxy in
Color.clear.contentShape(Path(CGRect(origin: .zero, size: proxy.size)))
}
Here is the component
struct InvisibleButton: View {
let action: (() -> Void)?
var body: some View {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
action?()
}
}
}
usage: Put your view and InbisibleButton in ZStack
ZStack {
**yourView()**
InvisibleButton {
print("Invisible button tapped")
}
}
you also can make a modifier to simplify usage:
struct InvisibleButtonModifier: ViewModifier {
let action: (() -> Void)?
func body(content: Content) -> some View {
ZStack {
content
InvisibleButton(action: action)
}
}
}
**yourView()**
.modifier(InvisibleButtonModifier {
print("Invisible button tapped")
})
However, if your SwiftUI View has a UIKit view as a subview under, you will have to set Color.gray.opacity(0.0001) in order to UIView's touches be ignored
In my case a View that didn't trigger onTapGesture:
struct MainView: View {
var action: () -> Void
var body: some View {
NotTappableView()
.contentShape(Rectangle())
.onTapGesture(
action()
)
}
}
I solved this way:
struct MainView: View {
var action: () -> Void
var body: some View {
NotTappableView()
.overlay(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
action()
}
)
}
}
This made whole untappable view now tappable.

Resources