Swiftui: How to Make Entire Shape Recognize Gestures When Stroked

SwiftUI: How to make entire shape recognize gestures when stroked?

You need to define the hit area, with modifier .contentShape():

struct ConfirmButton: View {
var action: () -> Void

var body: some View {
ZStack {
Circle()
.stroke(Color.purple, lineWidth: 10.0)
.padding(5)
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200, alignment: .center)
}.contentShape(Circle())
.gesture(
TapGesture()
.onEnded {
print("Hello world")
self.action()
}
)
}
}

how can I make onTapGesture works only if user tap on circle not inside all frame of Circle in SwiftUI?

A nice little hack would be to add a background layer to the circle (it can't be 100% clear or it wouldn't render, so I made it opacity 0.0001 which looks clear) and then add another tapGesture onto that layer. The new gesture will take priority in the background area and we can just leave it without an action so nothing happens.

Circle()
.fill(Color.red)
.frame(width: 100, height: 100, alignment: .center)
.background(
Color.black.opacity(0.0001).onTapGesture { }
)
.onTapGesture {
print("tap")
}

SwiftUI: How to draw filled and stroked shape?

You can also use strokeBorder and background in combination.

Code:

Circle()
.strokeBorder(Color.blue,lineWidth: 4)
.background(Circle().foregroundColor(Color.red))

Result:


Tap Action not working when Color is clear SwiftUI

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

.contentShape losing effect when overlayed

I did an extra little bit of debugging and realized it was working fine. The behaviour is different, though, depending on whether the circle is behind or not.

Here is my extra "debugging":

ZStack {
Circle()
.offset(y: -200)
.gesture(TapGesture().onEnded { print("TAPPED CIRCLE") } )

let path = Path() { path in
path.move(to: CGPoint(x: 250, y: 0))
path.addLine(to: CGPoint(x: 250, y: 1000))
}

path
.stroke(Color.green, style: StrokeStyle(lineWidth: 40))

path
.stroke(Color.blue, style: StrokeStyle(lineWidth: 2))
.contentShape(path.stroke(style: StrokeStyle(lineWidth: 40)))
.gesture(TapGesture().onEnded { print("TAPPED PATH") })
}

All this is doing is adding an extra path, drawing with a stroke the same lineWidth as the contentShape so I can see where the clicks are changing from registering and not registering on the path.

Where the path is over the circle, the hit detection is actually better than where it is not.

It seems that when there isn't a shape underneath, also accepting gesture interaction, there is an additional buffer to where the gestures are recognized.

Losing the buffer, and hence changing the behaviour, is what was making me think there was a problem.

DragGesture blocks touching a Slider in SwiftUI

See if this solution works for you. As mentioned in my comment it's basically overwriting the gesture reader of the slider completely. To get the width I used a geometry reader. You could probably even use .gesture or even .highPriorityGesture instead of simultaneousGesture Also depending where you place the GeometryReader you might have to use the .local coordinateSpace of the gesture.

struct ContentView: View {

@State private var progress: TimeInterval = 0
@State private var sliderMoving: Bool = false


var body: some View {

GeometryReader { geometry in

Slider(value: $progress, in: 0 ... Double(100), onEditingChanged: { didChange in

}).simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
print("gesture onChanged")
sliderMoving = true
progress = TimeInterval(gesture.location.x / (geometry.size.width / 100))
}
.onEnded { gesture in
print("gesture onEnded")
sliderMoving = false
progress = TimeInterval(gesture.location.x / (geometry.size.width / 100))
}
)

}
}
}

Update with addition to comment below. This optionally adjusts to the padding modifier and knob size. Depending how the slider is setup different adjustments might be needed to get the exact position. I'm not currently aware of a way to get the exact locations of individual parts of the slider. A custom slider might solve this problem.

struct ContentView6: View {

@State private var progress: TimeInterval = 0
@State private var sliderMoving: Bool = false

var body: some View {

GeometryReader { geometry in

let padding: CGFloat = 0 //optional in case padding needs to be adjusted.
let adjustment: CGFloat = padding + 15

Slider(value: $progress, in: 0 ... Double(100), onEditingChanged: { didChange in

})
.padding(padding)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
sliderMoving = true
progress = TimeInterval( min(max((gesture.location.x - adjustment) / ((geometry.size.width - adjustment*2) / 100), 0), 100) )
print(progress)
}
.onEnded { gesture in
sliderMoving = false
}
)

}
}
}

How do you detect a SwiftUI touchDown event with no movement or duration?

If you combine the code from these two questions:

How to detect a tap gesture location in SwiftUI?

UITapGestureRecognizer - make it work on touch down, not touch up?

You can make something like this:

ZStack {
Text("Test")
TapView {
print("Tapped")
}
}
struct TapView: UIViewRepresentable {
var tappedCallback: (() -> Void)

func makeUIView(context: UIViewRepresentableContext<TapView>) -> TapView.UIViewType {
let v = UIView(frame: .zero)
let gesture = SingleTouchDownGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}

class Coordinator: NSObject {
var tappedCallback: (() -> Void)

init(tappedCallback: @escaping (() -> Void)) {
self.tappedCallback = tappedCallback
}

@objc func tapped(gesture:UITapGestureRecognizer) {
self.tappedCallback()
}
}

func makeCoordinator() -> TapView.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}

func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<TapView>) {
}
}

class SingleTouchDownGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if self.state == .possible {
self.state = .recognized
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
self.state = .failed
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
self.state = .failed
}
}

There's definitely some abstractions we can make so that the usage is more like the other SwiftUI Gestures, but this is a start. Hopefully Apple builds in support for this at some point.

IOS - How to hide a view by touching anywhere outside of it

In touch began you should write like

override func touchesBegan(_ touches: Set<AnyHashable>, withEvent event: UIEvent) {
var touch: UITouch? = touches.first
//location is relative to the current view
// do something with the touched point
if touch?.view != yourView {
yourView.isHidden = true
}
}


Related Topics



Leave a reply



Submit