onTapGesure not registering for background of ScrollView - swift

For some reason, the .onTapGesture event won't fire for the background of the ScrollView, but it does fire correctly when applied to the background of a nested VStack view:
var body: some View {
ScrollView {
VStack {...}
.background(
Rectangle()
.fill(.red)
.onTapGesture {
print("!!!CLICKED!!!")
}
)
}
.background(
Rectangle()
.fill(.green)
.onTapGesture {
print("!!!CLICKED!!!")
}
)
}
So when I click the red area, I'm able to see "!!!CLICKED!!!" printed, but when I click the green area, nothing is printed:
ViewLayout image

To trigger the action on the background of the ScrollView, just move the .onTapGesture() modifier to the bottom of the ScrollView itself, instead of the .background().
Here below, you will see a sample code that works (I added a couple of variables to notice the effect directly on the screen):
#State private var clicked = "Nothing" // For testing purposes only
#State private var alternate = true // For testing purposes only
var body: some View {
ScrollView {
VStack {
Text("One")
Text("Two")
Text("Three")
Text(clicked) // For testing purposes: see what was clicked on the screen
.padding()
Text(alternate ? "Clicked" : "... and again") // See the effect after each click
}
.background(
Rectangle()
.fill(.red)
.onTapGesture {
clicked = "Clicked red"
alternate.toggle()
print("!!!CLICKED RED!!!")
}
)
}
// Here is where the modifier should be to trigger the action on the background of the ScrollView
.onTapGesture {
clicked = "Clicked green"
alternate.toggle()
print("!!!CLICKED GREEN!!!")
}
.background(
Rectangle()
.fill(.green)
)
}

Related

How to create a view that functions like Alert in SwiftUI on watchOS?

I want to have an alert style view that can include an icon image at the top along with the title.
alert(isPresented:content:) used with Alert does not seem to support adding images in any way. However, other than that limitation, it functions as I want my alert to function. It covers the status bar and blurs the main view as the background for the alert.
I attempted to use fullScreenCover(isPresented:onDismiss:content:) instead, but this approach does not behave like an alert as far as covering up EVERYTHING including the status bar, and blurring the main view as the background.
I have tried this so far, with the following result. One of the reasons I want it to behave like a normal Alert is so that the content can scroll without overlapping the clock time.
struct AlertView: View {
#EnvironmentObject var dataProvider: DataProvider
var alert: Watch.AlertMessage
var body: some View {
ScrollView {
Image("IconAlert")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.accentColor)
Text("Test Title")
.bold()
Text("Test Description")
Button("DISMISS") { print("dismiss") }
}
.padding()
.ignoresSafeArea()
.navigationBarHidden(true)
}
}
Try this this sample custom alert. It may give you some great approach(Code is below the image):
struct CustomAlert: View {
#State var isClicked = false
var body: some View {
ZStack {
Color.orange.edgesIgnoringSafeArea(.all)
Button("Show Alert") {
withAnimation {
isClicked.toggle()
}
}
ZStack {
if isClicked {
RoundedRectangle(cornerRadius: 20)
.fill(.thickMaterial)
.frame(width: 250, height: 250)
.transition(.scale)
VStack {
Image("Swift")
.resizable()
.frame(width: 150, height: 150)
.mask(RoundedRectangle(cornerRadius: 15))
Button("Dismiss") {
withAnimation {
isClicked.toggle()
}
}
.foregroundColor(.red)
}
.transition(.scale)
}
}
}
}
}

SwiftUI Button Not Clickable With Overlay

I have a Button with an overlay, but it is not clickable. If I take the overlay off, it becomes clickable. How can I make it work with the overlay?
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
UIPasteboard.general.string = viewModel.promoCode?.code ?? ""
} label: {
HStack {
Text(viewModel.promoCode?.code ?? "")
.foregroundColor(Color(UIColor.uStadium.primary))
.font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
.padding(.leading, 20)
Spacer()
Image("copyIcon")
.foregroundColor(Color(UIColor.uStadium.primary))
.padding(.trailing, 20)
}
}
.overlay (
RoundedRectangle(cornerRadius: 8)
.stroke(Color(UIColor.uStadium.primary), style: StrokeStyle(lineWidth: 2, dash: [10]))
.frame(height: 42)
.background(Color(UIColor.uStadium.primary.withAlphaComponent(0.2)))
)
.frame(height: 42)
.cornerRadius(8)
Another solution is adding .allowsHitTesting(false) modifier at the view inside of the overlay.
The reason why touch is not working is overlay modifier layers a secondary view in front of current view. So when user tap the button they were actually tapping the overlayed view.
The allowsHitTesting(false) stop a view from receiving any kind of taps, and any taps automatically continue through the view on to whatever is behind it.
Simple example:
struct ContentView: View {
var body: some View {
Button {
print("dd")
} label: {
Image(systemName: "house").resizable().frame(width: 50, height: 50)
}
.overlay (
RoundedRectangle(cornerRadius: 8).allowsHitTesting(false)
)
}
}
and touch works!

Disable to Change Page On Swipe of Tab View in SwiftUI

I am using Tab View in my SwiftUI app. I want the changing of page disabled, while swiping left or right. And I had achieved it from this. This works fine but the issue I am facing is I have a button on the bottom of every view, and when I try to swipe from the button, it is swiping left right. I want to disable it too but don't know how to do it. Here is my code:
struct TabViewTesting: View {
#State var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
firstView().tag(0).contentShape(Rectangle()).gesture(DragGesture())
secondView().tag(1).contentShape(Rectangle()).gesture(DragGesture())
thirdView().tag(2).contentShape(Rectangle()).gesture(DragGesture())
}.tabViewStyle(.page(indexDisplayMode: .never))
}
}
And this is the code for the Views:
extension TabViewTesting {
func firstView() -> some View {
VStack {
Text("First screen")
Spacer()
Button {
self.tabSelection = 1
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to 2nd view")
.foregroundColor(.white)
}
}.padding()
}.background(.green)
}
func secondView() -> some View {
VStack {
Text("second screen")
Spacer()
Button {
self.tabSelection = 2
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to 3rd view")
.foregroundColor(.white)
}
}.padding()
}.background(.red)
}
func thirdView() -> some View {
VStack {
Text("Third screen")
Spacer()
Button {
self.tabSelection = 0
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to first view")
.foregroundColor(.white)
}
}.padding()
}.background(.yellow)
}
}
And this is what happening:
I actually found an answer in the comments of this question.
The issue is: any view that has an "onTapGesture" will ignore ".gesture(DragGesture())".
The solution is to use ".simulataneousGesture(DragGesture())" instead to ensure the gesture is capture and handled by both view/modifier.
It worked perfectly in my case after changing it. The only exception is for 2 finger drag gesture.

Dock an element with ScrollView when it reaches the top of screen

Take the following image as an example. The entire screen underneath the header including image crown, person image and text life is a ScrollView. What I want is that what the Text "Tasks" reaches the region of "Life", it stops going up, which makes it look like docking at the top. Also, I hope "Tasks" could be a button, so we could always skip back to the task list as soon as we tap it.
Thus, what element should I use in SwiftUI to achieve so?
If you have a LazyVStack inside the scroll view you can implement a section with a header for the crown, text and person icon ( those 3 will be in an HStack) and after that the lazy stacks have a parameter called pinnedViews where you will put .sectionHeaders. That should pin the HStack to the top when you scroll up.
var body: some View {
ZStack {
Color.red
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section(header:
HStack {
Button(action: { }) {
Text("Crown")
.foregroundColor(contentSelection == .one ? .black : .gray)
}
Spacer()
Button(action: { }) {
Text("Life")
.foregroundColor(contentSelection == .two ? .black : .gray)
}
Spacer()
Button(action: { }) {
Text("Person")
}
}
.padding()
.background(Color.green)
) {
VStack {
ForEach((1...50), id: \.self) { item in
ZStack {
Text("This is item number \(item)")
}
.frame(height: 80)
}
}
}
}
}
}
}

SwiftUI - button added has text auto-capitalized and is tappable in the whole view

I'm trying to implement a view using SwiftUI. Here is the basic structure:
var body: some View {
List {
ForEach(notifications!, id: \.self) { notification in
if let text = NotificationManager.shared.notificationText(notification: notification) {
VStack(alignment: .leading, spacing: Constants.VStack.spacing) {
HStack {
if notification.successfullyUploaded == false {
Image(systemName: Constants.Images.failure).foregroundColor(Color(UIColor.alert))
} else {
Image(systemName: Constants.Images.success).foregroundColor(Color(UIColor.success))
}
Text(text).font(.system(size: Constants.FontSize.standard))
}
let secsAgo = notification.timeStamp?.timeIntervalSinceNow
if let secsAgo = secsAgo {
HStack {
Text("\(getTimeString(secsAgo: secsAgo))").font(.system(size: Constants.FontSize.timeStamp)).foregroundColor(Color(UIColor.blue))
if notification.successfullyUploaded == false {
Spacer()
Button {
print("Test")
} label: {
Text("Amend submitted details").autocapitalization(.none)
}
}
}
}
}
}
}.onDelete(perform: delete)
}.cornerRadius(Constants.VStack.cornerRadius)
.shadow(color: .gray, radius: Constants.Shadow.radius, x: Constants.Shadow.x, y: Constants.Shadow.y)
.frame(width: Constants.VStack.width, height: Constants.VStack.height, alignment: .center)
}
I'm having trouble with the button I am trying to add. So essentially, this needs to be placed to the right hand side of the view and only this button should be tappable.
What I'm getting are 2 things I don't want:
1- the text is showing in all caps (hence why I tried adding .autocapitalization(.none)
2- the entire view reacts to the tap event when I actually only want to button itself.
Here is a screenshot of the view as it currently is:
Wherever I tap in the list item the print statement is triggered.