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
Swift Protocol to Require Properties as Protocol
How to Read a Property List from Data in Swift 3
Pickers Are Overlapping in iOS 15 Preventing Some of Them to Be Scrolled
Unable to Save On/Off State of a Uitableviewcell
How to Declare Swift Implicitly Unwrapped Optional as a Constant
Scngeometryelement Setup in Swift 3
Why Is the Alert Triggering Out of Order If Value Is Not Updated
How to Change the Z Index or Stack Order of Uiview
Swift Task Continuation Misuse: Leaked Its Continuation - for Delegate
Iphonex Not Call Prefersstatusbarhidden
Swift: When Should I Use "Var" Instead of "Let"
Fblpromises Framework Not Found
How Do Generators Whose Element Is Optional Know When They'Ve Reached the End
For Loop for Dictionary Don't Follow It's Order in Swift
How to Distinguish Between Multiple Uipickerviews on One Page