SwiftUI on macOS - handle single-click and double-click at the same time - swift

Consider this view in SwiftUI:
struct MyView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
.onTapGesture {
print("single clicked")
}
}
}
Now we're handling single-click. Say you wanna handle double-click too, but with a separate callback.
You have 2 options:
Add double click handler after single click - this doesn't work at all
Add double click handler before single click handler, this kinda works:
struct MyView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
.onTapGesture(count: 2) {
print("double clicked")
}
.onTapGesture {
print("single clicked")
}
}
}
Double-click handler is called properly, but single-click handler is called after a delay of about 250ms.
Any ideas how to resolve this?

Here is possible approach (tested with Xcode 11.2 / macOS 10.15)
struct MyView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
.gesture(TapGesture(count: 2).onEnded {
print("double clicked")
})
.simultaneousGesture(TapGesture().onEnded {
print("single clicked")
})
}
}

The gesture-based solutions don't work correctly when selection is enabled on the List, since the first tap will delay the actual selection. I have made a double click modifier which works on any view and seems to solve it the way I expect in all cases:
extension View {
/// Adds a double click handler this view (macOS only)
///
/// Example
/// ```
/// Text("Hello")
/// .onDoubleClick { print("Double click detected") }
/// ```
/// - Parameters:
/// - handler: Block invoked when a double click is detected
func onDoubleClick(handler: #escaping () -> Void) -> some View {
modifier(DoubleClickHandler(handler: handler))
}
}
struct DoubleClickHandler: ViewModifier {
let handler: () -> Void
func body(content: Content) -> some View {
content.overlay {
DoubleClickListeningViewRepresentable(handler: handler)
}
}
}
struct DoubleClickListeningViewRepresentable: NSViewRepresentable {
let handler: () -> Void
func makeNSView(context: Context) -> DoubleClickListeningView {
DoubleClickListeningView(handler: handler)
}
func updateNSView(_ nsView: DoubleClickListeningView, context: Context) {}
}
class DoubleClickListeningView: NSView {
let handler: () -> Void
init(handler: #escaping () -> Void) {
self.handler = handler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if event.clickCount == 2 {
handler()
}
}
}
https://gist.github.com/joelekstrom/91dad79ebdba409556dce663d28e8297

Related

How can I detect press gesture within ButtonStyle in SwiftUI? [duplicate]

I have a Button. I want to set custom background color for highlighted state. How can I do it in SwiftUI?
Button(action: signIn) {
Text("Sign In")
}
.padding(.all)
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
.font(Font.body.bold())
Updated for SwiftUI beta 5
SwiftUI does actually expose an API for this: ButtonStyle.
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(configuration.isPressed ? Color.red : Color.blue)
.cornerRadius(8.0)
}
}
// To use it
Button(action: {}) {
Text("Hello World")
}
.buttonStyle(MyButtonStyle())
As far as I can tell, theres no officially supported way to do this as of yet. Here is a little workaround that you can use. This produces the same behavior as in UIKit where tapping a button and dragging your finger off of it will keep the button highlighted.
struct HoverButton<Label: View>: View {
private let action: () -> ()
private let label: () -> Label
init(action: #escaping () -> (), label: #escaping () -> Label) {
self.action = action
self.label = label
}
#State private var pressed: Bool = false
var body: some View {
Button(action: action) {
label()
.foregroundColor(pressed ? .red : .blue)
.gesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in self.pressed = true }
.onEnded { _ in self.pressed = false })
}
}
}
I was looking for a similar functionality and I did it in the following way.
I created a special View struct returning a Button in the style I need, in this struct I added a State property selected. I have a variable named 'table' which is an Int since my buttons a round buttons with numbers on it
struct TableButton: View {
#State private var selected = false
var table: Int
var body: some View {
Button("\(table)") {
self.selected.toggle()
}
.frame(width: 50, height: 50)
.background(selected ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
Then I use in my content View the code
HStack(spacing: 10) {
ForEach((1...6), id: \.self) { table in
TableButton(table: table)
}
}
This creates an horizontal stack with 6 buttons which color blue when selected and red when deselected.
I am not a experienced developer but just tried all possible ways until I found that this is working for me, hopefully it is useful for others as well.
This is for the people who are not satisfied with the above solutions, as they raise other problems such as overlapping gestures(for example, it's quite hard to use this solution in scrollview now). Another crutch is to create a custom button style like this
struct CustomButtonStyle<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Self.Configuration) -> some View {
return change(configuration.isPressed)
}
}
So, we should just transfer the closure which will return the state of the button and create the button based on this parameter. It will be used like this:
struct CustomButton<Content>: View where Content: View {
var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
Button(action: { }, label: {
EmptyView()
})
.buttonStyle(CustomButtonStyle(change: { bool in
Text("\(bool ? "yo" : "yo2")")
}))
}
}
Okey let me clear everything again. Here is the exact solution
Create the below button modifier.
struct StateableButton<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Configuration) -> some View {
return change(configuration.isPressed)
}
}
Then use it like below one
Button(action: {
print("Do something")
}, label: {
// Don't create your button view in here
EmptyView()
})
.buttonStyle(StateableButton(change: { state in
// Create your button view in here
return HStack {
Image(systemName: "clock.arrow.circlepath")
Text(item)
Spacer()
Image(systemName: "arrow.up.backward")
}
.padding(.horizontal)
.frame(height: 50)
.background(state ? Color.black : Color.clear)
}))
You need to define a custom style that can be used to provide the two backgrounds for normal and highlighted states:
Button(action: {
print("action")
}, label: {
Text("My Button").padding()
})
.buttonStyle(HighlightableButtonStyle(normal: { Color.red },
highlighted: { Color.green }))
// Custom button style
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct HighlightableButtonStyle<N, H>: ButtonStyle where N: View, H: View {
private let alignment: Alignment
private let normal: () -> N
private let highlighted: () -> H
init(alignment: Alignment = .center, #ViewBuilder normal: #escaping () -> N, #ViewBuilder highlighted: #escaping () -> H) {
self.alignment = alignment
self.normal = normal
self.highlighted = highlighted
}
func makeBody(configuration: Configuration) -> some View {
return ZStack {
if configuration.isPressed {
configuration.label.background(alignment: alignment, content: normal)
}
else {
configuration.label.background(alignment: alignment, content: highlighted)
}
}
}
}

How to add SwiftUI custom style as a static extension

In swiftui3 you can use buttonstyle shortcut like so
Button("0") {print("pressed 0")}
.buttonStyle(.bordered)
I would like to do that with my custom buttonstyle class
struct CrazyButtonStyle:ButtonStyle{
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.red)
}
}
calling it like this
Button("0") {print("pressed 0")}
.buttonStyle(.crazy)
I have tried
extension ButtonStyle{
static var crazy:CrazyButtonStyle {
get {
return CrazyButtonStyle()
}
}
}
but im getting this error
Contextual member reference to static property 'crazy' requires 'Self' constraint in the protocol extension
extension ButtonStyle where Self == CrazyButtonStyle{
static var crazy:CrazyButtonStyle {
get {
return CrazyButtonStyle()
}
}
}
adding where Self to the extension seems to work. But im not sure if this is the best way.
Apple's suggested way:
struct CrazyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.red)
}
}
extension ButtonStyle where Self == CrazyButtonStyle {
static var crazy: Self { Self() }
}
Usage:
Button(action: {}) {
Image(systemName: "forward.fill")
}
.buttonStyle(.crazy)
Source: Develop Apps for iOS Tutorial
You can define something like the following:
struct CrazyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Spacer()
configuration.label.foregroundColor(.red)
Spacer()
}
.scaleEffect(configuration.isPressed ? 0.90 : 1)
}
}
Then, to apply it to a Button:
Button("Crazy Button") {}
.buttonStyle(CrazyButtonStyle())
See swift docs for reference.
Also here are some other examples, with animations as well!
/// define your style
public struct MainButtonStyle: ButtonStyle {
public init(){}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(height: 50, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color(.primary).clipShape(RoundedRectangle(cornerRadius: 4)))
.foregroundColor(Color.white)
.scaleEffect(configuration.isPressed ? 0.95: 1)
}
}
/// create an enum of all new types you've just created
public enum ButtonStyles {
case main
}
/// add a new functionality to the view
public extension View {
#ViewBuilder
func buttonStyle(_ style: ButtonStyles) -> some View {
switch style {
case .main:
self.buttonStyle(MainButtonStyle())
}
}
}
then you can use it like this
Button { someAction() } label: {Text("COPY")}
.buttonStyle(.main)

How to detect a 'Click' gesture in SwiftUI tvOS

Using:
SwiftUI
Swift 5
tvOS
Xcode Version 11.2.1
I just want to detect a click gesture on the URLImage below
JFYI I am very new to Xcode, Swift and SwiftUI (less than 3 weeks).
URLImage(URL(string: channel.thumbnail)!,
delay: 0.25,
processors: [ Resize(size: CGSize(width:isFocused ? 300.0 : 225.0, height:isFocused ? 300.0 : 225.0), scale: UIScreen.main.scale) ],
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
})
.frame(width: isFocused ? 300.0 : 250.0, height:isFocused ? 300.0 : 250.0)
.clipShape(Circle())
.overlay(
Circle().stroke( isFocused ? Color.white : Color.black, lineWidth: 8))
.shadow(radius:5)
.focusable(true, onFocusChange:{ (isFocused) in
withAnimation(.easeInOut(duration:0.3)){
self.isFocused = isFocused
}
if(isFocused){
self.manager.bannerChannel = self.channel
print(self.manager.bannerChannel)
self.manager.loadchannelEPG(id: self.channel.id)
}
})
.padding(20)
}
The only workaround I have found is wrapping it in a NavigationLink
or a Button but then focusable on the button doesn't run.
I found out that focusable runs on a Button/NavigationLink if I add corner radius to it but then the default click action doesn't run
Also, TapGesture is not available in tvOS
Since Gestures are available maybe there is a way using gestures that I cannot figure out.
OR
If there is a way to tap into focusable on a button (although this is the less favoured alternative since this changes the look I want to achieve).
Edit: onTapGesture() is now available starting in tvOS 16
tvOS 16
struct ContentView: View {
#FocusState var focused1
#FocusState var focused2
var body: some View {
HStack {
Text("Clickable 1")
.foregroundColor(self.focused1 ? Color.red : Color.black)
.focusable(true)
.focused($focused1)
.onTapGesture {
print("clicked 1")
}
Text("Clickable 2")
.foregroundColor(self.focused2 ? Color.red : Color.black)
.focusable(true)
.focused($focused2)
.onTapGesture {
print("clicked 2")
}
}
}
}
Previous Answer for tvOS 15 and earlier
It is possible, but not for the faint of heart. I came up with a somewhat generic solution that may help you. I hope in the next swiftUI update Apple adds a better way to attach click events for tvOS and this code can be relegated to the trash bin where it belongs.
The high level explanation of how to do this is to make a UIView that captures the focus and click events, then make a UIViewRepresentable so swiftUI can use the view. Then the view is added to the layout in a ZStack so it's hidden, but you can receive focus and respond to click events as if the user was really interacting with your real swiftUI component.
First I need to make a UIView that captures the events.
class ClickableHackView: UIView {
weak var delegate: ClickableHackDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
}
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if event?.allPresses.map({ $0.type }).contains(.select) ?? false {
delegate?.clicked()
} else {
superview?.pressesEnded(presses, with: event)
}
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
delegate?.focus(focused: isFocused)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var canBecomeFocused: Bool {
return true
}
}
The clickable delegate:
protocol ClickableHackDelegate: class {
func focus(focused: Bool)
func clicked()
}
Then make a swiftui extension for my view
struct ClickableHack: UIViewRepresentable {
#Binding var focused: Bool
let onClick: () -> Void
func makeUIView(context: UIViewRepresentableContext<ClickableHack>) -> UIView {
let clickableView = ClickableHackView()
clickableView.delegate = context.coordinator
return clickableView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ClickableHack>) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, ClickableHackDelegate {
private let control: ClickableHack
init(_ control: ClickableHack) {
self.control = control
super.init()
}
func focus(focused: Bool) {
control.focused = focused
}
func clicked() {
control.onClick()
}
}
}
Then I make a friendlier swiftui wrapper so I can pass in any kind of component I want to be focusable and clickable
struct Clickable<Content>: View where Content : View {
let focused: Binding<Bool>
let content: () -> Content
let onClick: () -> Void
#inlinable public init(focused: Binding<Bool>, onClick: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.focused = focused
self.onClick = onClick
}
var body: some View {
ZStack {
ClickableHack(focused: focused, onClick: onClick)
content()
}
}
}
Example usage:
struct ClickableTest: View {
#State var focused1: Bool = false
#State var focused2: Bool = false
var body: some View {
HStack {
Clickable(focused: self.$focused1, onClick: {
print("clicked 1")
}) {
Text("Clickable 1")
.foregroundColor(self.focused1 ? Color.red : Color.black)
}
Clickable(focused: self.$focused2, onClick: {
print("clicked 2")
}) {
Text("Clickable 2")
.foregroundColor(self.focused2 ? Color.red : Color.black)
}
}
}
}
If you'd like to avoid UIKit, you can achieve the desired solution with Long Press Gesture by setting a really small duration of pressing.
1. Only Press:
If you only need to handle the pressing action and don't need long pressing at all.
ContentView()
.onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) {
print("pressed")
}
2. Press and Long press:
If you need to handle both pressing and Long pressing.
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.5)
.onEnded { _ in
print("longpress")
}
}
ContentView()
.highPriorityGesture(longPress)
.onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) {
print("press")
}

SwiftUI Button interact with Map

I'm totally new with Swift and SwiftUI and for a project group, I need to develop my first IOS app.
I can display a map with Mapbox but I don't know how to follow my user when I click on a button.
I don't know how to interact my button with my struct MapView
This is my code:
MapView.swift:
import Mapbox
struct MapView: UIViewRepresentable {
let mapView: MGLMapView = MGLMapView(frame: .zero)
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
}
func styleURL(_ styleURL: URL) -> MapView {
mapView.styleURL = styleURL
return self
}
func centerCoordinate(_ centerCoordinate: CLLocationCoordinate2D) -> MapView {
mapView.centerCoordinate = centerCoordinate
return self
}
func zoomLevel(_ zoomLevel: Double) -> MapView {
mapView.zoomLevel = zoomLevel
return self
}
func userTrackingMode(_ userTrackingMode: MGLUserTrackingMode) -> MapView {
mapView.userTrackingMode = userTrackingMode
return self
}
}
class Coordinator: NSObject, MGLMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
}
ContentView.swift:
import Mapbox
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
ZStack {
MapView()
.userTrackingMode(.follow)
.edgesIgnoringSafeArea(.all)
HStack(alignment: .top) {
Spacer()
VStack() {
Button(action: {
//ACTION TO CHANGE FOLLOW MODE
}) {
Image(systemName: "location.fill")
.frame(width: 40.0, height: 40.0)
}
.padding(.top, 60.0)
.padding(.trailing, 10.0)
.frame(width: 45.0, height: 80.0)
Spacer()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.colorScheme, .dark)
}
}
Actually I think in your case, as you have a reference to map, you can try to interact with it directly (aka imperatively, because it is such by nature, so no need to make simple thing complex)
Like
...
let myMapHolder = MapView()
var body: some View {
ZStack {
myMapHolder
.userTrackingMode(.follow)
.edgesIgnoringSafeArea(.all)
...
VStack() {
Button(action: {
self.myMapHolder.mapView.userTrackingMode = _your_mode_
}) {
Image(systemName: "location.fill")

How to detect a tap gesture location in SwiftUI?

(For SwiftUI, not vanilla UIKit)
Very simple example code to, say, display red boxes on a gray background:
struct ContentView : View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack {
Color.gray
.tapAction {
// TODO: add an entry to self.points of the location of the tap
}
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
I'm assuming instead of tapAction, I need to have a TapGesture or something? But even there I don't see any way to get information on the location of the tap. How would I go about this?
Well, after some tinkering around and thanks to this answer to a different question of mine, I've figured out a way to do it using a UIViewRepresentable (but by all means, let me know if there's an easier way!) This code works for me!
struct ContentView : View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack(alignment: .topLeading) {
Background {
// tappedCallback
location in
self.points.append(location)
}
.background(Color.white)
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
struct Background:UIViewRepresentable {
var tappedCallback: ((CGPoint) -> Void)
func makeUIView(context: UIViewRepresentableContext<Background>) -> UIView {
let v = UIView(frame: .zero)
let gesture = UITapGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}
class Coordinator: NSObject {
var tappedCallback: ((CGPoint) -> Void)
init(tappedCallback: #escaping ((CGPoint) -> Void)) {
self.tappedCallback = tappedCallback
}
#objc func tapped(gesture:UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point)
}
}
func makeCoordinator() -> Background.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<Background>) {
}
}
I was able to do this with a DragGesture(minimumDistance: 0). Then use the startLocation from the Value on onEnded to find the tap's first location.
Update iOS 16
Starting form iOS 16 / macOS 13, the onTapGesture modifier makes available the location of the tap/click in the action closure:
struct ContentView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.onTapGesture { location in
print("Tapped at \(location)")
}
}
}
Original Answser
The most correct and SwiftUI-compatible implementation I come up with is this one. You can use it like any regular SwiftUI gesture and even combine it with other gestures, manage gesture priority, etc...
import SwiftUI
struct ClickGesture: Gesture {
let count: Int
let coordinateSpace: CoordinateSpace
typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
precondition(count > 0, "Count must be greater than or equal to 1.")
self.count = count
self.coordinateSpace = coordinateSpace
}
var body: SimultaneousGesture<TapGesture, DragGesture> {
SimultaneousGesture(
TapGesture(count: count),
DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
)
}
func onEnded(perform action: #escaping (CGPoint) -> Void) -> _EndedGesture<ClickGesture> {
self.onEnded { (value: Value) -> Void in
guard value.first != nil else { return }
guard let location = value.second?.startLocation else { return }
guard let endLocation = value.second?.location else { return }
guard ((location.x-1)...(location.x+1)).contains(endLocation.x),
((location.y-1)...(location.y+1)).contains(endLocation.y) else {
return
}
action(location)
}
}
}
The above code defines a struct conforming to SwiftUI Gesture protocol. This gesture is a combinaison of a TapGesture and a DragGesture. This is required to ensure that the gesture was a tap and to retrieve the tap location at the same time.
The onEnded method checks that both gestures occurred and returns the location as a CGPoint through the escaping closure passed as parameter. The two last guard statements are here to handle multiple tap gestures, as the user can tap slightly different locations, those lines introduce a tolerance of 1 point, this can be changed if ones want more flexibility.
extension View {
func onClickGesture(
count: Int,
coordinateSpace: CoordinateSpace = .local,
perform action: #escaping (CGPoint) -> Void
) -> some View {
gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded(perform: action)
)
}
func onClickGesture(
count: Int,
perform action: #escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: count, coordinateSpace: .local, perform: action)
}
func onClickGesture(
perform action: #escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: 1, coordinateSpace: .local, perform: action)
}
}
Finally View extensions are defined to offer the same API as onDragGesture and other native gestures.
Use it like any SwiftUI gesture:
struct ContentView : View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack {
Color.gray
.onClickGesture { point in
points.append(point)
}
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
An easy solution is to use the DragGesture and set minimumDistance parameter to 0 so that it resembles the tap gesture:
Color.gray
.gesture(DragGesture(minimumDistance: 0).onEnded({ (value) in
print(value.location) // Location of the tap, as a CGPoint.
}))
In case of a tap gesture it will return the location of this tap. However, it will also return the end location for a drag gesture – what's also referred to as a "touch up event". Might not be the desired behavior, so keep it in mind.
It is also possible to use gestures.
There is a few more work to cancel the tap if a drag occurred or trigger action on tap down or tap up..
struct ContentView: View {
#State var tapLocation: CGPoint?
#State var dragLocation: CGPoint?
var locString : String {
guard let loc = tapLocation else { return "Tap" }
return "\(Int(loc.x)), \(Int(loc.y))"
}
var body: some View {
let tap = TapGesture().onEnded { tapLocation = dragLocation }
let drag = DragGesture(minimumDistance: 0).onChanged { value in
dragLocation = value.location
}.sequenced(before: tap)
Text(locString)
.frame(width: 200, height: 200)
.background(Color.gray)
.gesture(drag)
}
}
Just in case someone needs it, I have converted the above answer into a view modifier which also takes a CoordinateSpace as an optional parameter
import SwiftUI
import UIKit
public extension View {
func onTapWithLocation(coordinateSpace: CoordinateSpace = .local, _ tapHandler: #escaping (CGPoint) -> Void) -> some View {
modifier(TapLocationViewModifier(tapHandler: tapHandler, coordinateSpace: coordinateSpace))
}
}
fileprivate struct TapLocationViewModifier: ViewModifier {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
func body(content: Content) -> some View {
content.overlay(
TapLocationBackground(tapHandler: tapHandler, coordinateSpace: coordinateSpace)
)
}
}
fileprivate struct TapLocationBackground: UIViewRepresentable {
var tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
func makeUIView(context: UIViewRepresentableContext<TapLocationBackground>) -> UIView {
let v = UIView(frame: .zero)
let gesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}
class Coordinator: NSObject {
var tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
init(handler: #escaping ((CGPoint) -> Void), coordinateSpace: CoordinateSpace) {
self.tapHandler = handler
self.coordinateSpace = coordinateSpace
}
#objc func tapped(gesture: UITapGestureRecognizer) {
let point = coordinateSpace == .local
? gesture.location(in: gesture.view)
: gesture.location(in: nil)
tapHandler(point)
}
}
func makeCoordinator() -> TapLocationBackground.Coordinator {
Coordinator(handler: tapHandler, coordinateSpace: coordinateSpace)
}
func updateUIView(_: UIView, context _: UIViewRepresentableContext<TapLocationBackground>) {
/* nothing */
}
}
Using some of the answers above, I made a ViewModifier that is maybe useful:
struct OnTap: ViewModifier {
let response: (CGPoint) -> Void
#State private var location: CGPoint = .zero
func body(content: Content) -> some View {
content
.onTapGesture {
response(location)
}
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onEnded { location = $0.location }
)
}
}
extension View {
func onTapGesture(_ handler: #escaping (CGPoint) -> Void) -> some View {
self.modifier(OnTap(response: handler))
}
}
Then use like so:
Rectangle()
.fill(.green)
.frame(width: 200, height: 200)
.onTapGesture { location in
print("tapped: \(location)")
}
Using DragGesture with minimumDistance broke scroll gestures on all the views that are stacked under. Using simultaneousGesture did not help. What ultimately did it for me was using sequencing the DragGesture to a TapGesture inside simultaneousGesture, like so:
.simultaneousGesture(TapGesture().onEnded {
// Do something
}.sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { value in
print(value.startLocation)
}))
In iOS 16 and MacOS 13 there are better solutions, but to stay compatible with older os versions, I use this rather simple gesture, which also has the advantage of distinguish between single- and double-click.
var combinedClickGesture: some Gesture {
SimultaneousGesture(ExclusiveGesture(TapGesture(count: 2),TapGesture(count: 1)), DragGesture(minimumDistance: 0) )
.onEnded { value in
if let v1 = value.first {
var count: Int
switch v1 {
case .first(): count = 2
case .second(): count = 1
}
if let v2 = value.second {
print("combinedClickGesture couunt = \(count) location = \(v2.location)")
}
}
}
}
As pointed out several times before it´s a problem when the view already is using DragGesture, but often it is fixed when using the modifier:
.simultaneousGesture(combinedClickGesture)
instead of
.gesture(combinedClickGesture)
Posting this for others who still have to support iOS 15.
It's also possible using GeometryReader and CoordinateSpace. The only downside is depending on your use case you might have to specify the size of the geometry reader.
VStack {
Spacer()
GeometryReader { proxy in
Button {
print("Global tap location: \(proxy.frame(in: .global).center)")
print("Custom coordinate space tap location: \(proxy.frame(in: .named("StackOverflow")))")
} label: {
Text("Tap me I know you want it")
}
.frame(width: 42, height: 42)
}
.frame(width: 42, height: 42)
Spacer()
}
.coordinateSpace(name: "StackOverflow")