How to handle touch gestures in SwiftUI in Swift UIKit Map component?
@objc can't be applied to functions inside structs. Move the function inside the Coordinator and then change the UIGestureRecognizer declaration this way:
let gRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.triggerTouchAction(gestureReconizer:)))
Can't add Tap Gesture Recognizer to SwiftUI MKMapView UIViewRepresentable
In your Coordinator
, you have two references to MapView
s. One (control
) is set in init
and represents the actual view that you want. The other (parent
) is defined within your Coordinator
and is not actually part of the view hierarchy. Therefore, when you try to get a coordinate from it, it returns nil
. You can change all of the references to control
and it works:
class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
let sfCoord = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
init(_ control: MapView) {
self.control = control
}
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
if let annotationView = views.first {
if let annotation = annotationView.annotation {
if annotation is MKUserLocation {
let region = MKCoordinateRegion(center: annotation.coordinate, latitudinalMeters: 2000, longitudinalMeters: 2000)
mapView.setRegion(region, animated: true)
}
}
}
}//did add
@objc func addAnnotationOnTapGesture(sender: UITapGestureRecognizer) {
if sender.state == .ended {
print("in addAnnotationOnTapGesture")
let point = sender.location(in: control.myMapView)
print("point is \(point)")
let coordinate = control.myMapView?.convert(point, toCoordinateFrom: control.myMapView)
print("coordinate?.latitude is \(String(describing: coordinate?.latitude))")
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate ?? sfCoord
annotation.title = "Start"
control.myMapView?.addAnnotation(annotation)
}
}
}//coord
How to detect a 'Click' gesture in SwiftUI tvOS
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)
}
}
}
}
How to intercept touches events on a MKMapView or UIWebView objects?
The best way I have found to achieve this is with a Gesture Recognizer. Other ways turn out to involve a lot of hackish programming that imperfectly duplicates Apple's code, especially in the case of multitouch.
Here's what I do: Implement a gesture recognizer that cannot be prevented and that cannot prevent other gesture recognizers. Add it to the map view, and then use the gestureRecognizer's touchesBegan, touchesMoved, etc. to your fancy.
How to detect any tap inside an MKMapView (sans tricks)
WildcardGestureRecognizer * tapInterceptor = [[WildcardGestureRecognizer alloc] init];
tapInterceptor.touchesBeganCallback = ^(NSSet * touches, UIEvent * event) {
self.lockedOnUserLocation = NO;
};
[mapView addGestureRecognizer:tapInterceptor];
WildcardGestureRecognizer.h
//
// WildcardGestureRecognizer.h
// Copyright 2010 Floatopian LLC. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef void (^TouchesEventBlock)(NSSet * touches, UIEvent * event);
@interface WildcardGestureRecognizer : UIGestureRecognizer {
TouchesEventBlock touchesBeganCallback;
}
@property(copy) TouchesEventBlock touchesBeganCallback;
@end
WildcardGestureRecognizer.m
//
// WildcardGestureRecognizer.m
// Created by Raymond Daly on 10/31/10.
// Copyright 2010 Floatopian LLC. All rights reserved.
//
#import "WildcardGestureRecognizer.h"
@implementation WildcardGestureRecognizer
@synthesize touchesBeganCallback;
-(id) init{
if (self = [super init])
{
self.cancelsTouchesInView = NO;
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (touchesBeganCallback)
touchesBeganCallback(touches, event);
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void)reset
{
}
- (void)ignoreTouch:(UITouch *)touch forEvent:(UIEvent *)event
{
}
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
return NO;
}
- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
return NO;
}
@end
SWIFT 3
let tapInterceptor = WildCardGestureRecognizer(target: nil, action: nil)
tapInterceptor.touchesBeganCallback = {
_, _ in
self.lockedOnUserLocation = false
}
mapView.addGestureRecognizer(tapInterceptor)
WildCardGestureRecognizer.swift
import UIKit.UIGestureRecognizerSubclass
class WildCardGestureRecognizer: UIGestureRecognizer {
var touchesBeganCallback: ((Set<UITouch>, UIEvent) -> Void)?
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.cancelsTouchesInView = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
touchesBeganCallback?(touches, event)
}
override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
Recognizing touch events only inside the masked view
If you want to do this perfectly, use the UIGestureRecognizerDelegate
method gestureRecognizer(gesture, shouldReceiveTouch: touch) -> Bool
. You will need to map the given gesture recogniser to a particular hexagon and then do pixel precise hit-testing on the image for that hexagon. This latter part is achieved by rendering the mask image to a graphics context and finding the pixel at the point corresponding to the touch location.
However, this is likely overkill. You can simplify the problem by hit-testing each shape as a circle, not a hexagon. The circle shape roughly approximates the hexagon so it will work almost the same for a user and avoids messy pixel-level alpha equality. The inaccuracy of touch input will cover up the inaccurate regions.
Another option is to rework your views to be based on CAShapeLayer
masks. CAShapeLayer
includes a path
property. Bezier paths in UIKit include their own rolled versions of path-contains-point methods so you can just use that for this purpose.
Related Topics
Swift Draw Shadow to a Uibezier Path
Building User Database Model in Firebase
Dispatchqueue:Cannot Be Called with Ascopy = No on Non-Main Thread
Xcode Error: Missing Required Module 'Firebase'
Enumerateobjectsusingblock in Swift
How to Set Inputview for Textfield in Swiftui
How to Get the Download Progress with the New Try Await Urlsession.Shared.Download(...)
Case Insensitive Dictionary in Swift
How to Check If a String Contains Chinese in Swift
Swiftui - How to Use Oncommand with Nsmenuitem on MACos
Swift Tableview Cell Set Accessory Type
How to Add Caching to Asyncimage
How to Select a Contact with Abpeoplepickernavigationcontroller in Swift
Swift - Take Nil as Argument in Generic Function with Optional Argument
Swift: Dictionary Access via Index
Switch That Checks Nsindexpath's Row and Section