How to Handle Touch Gestures in Swiftui in Swift Uikit Map Component

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 MapViews. 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



Leave a reply



Submit