web-dev-qa-db-fra.com

Comment intercepter des événements de contact sur un objet MKMapView ou UIWebView?

Je ne suis pas sûr de ce que je fais mal mais j'essaie de saisir un objet MKMapView Je l'ai sous-classé en créant la classe suivante:

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface MapViewWithTouches : MKMapView {

}

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *) event;   

@end

Et la mise en place:

#import "MapViewWithTouches.h"
@implementation MapViewWithTouches

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *) event {

    NSLog(@"hello");
    //[super touchesBegan:touches   withEvent:event];

}
@end

Mais il semble que lorsque j'utilise cette classe, je ne vois rien sur la console: 

MapViewWithTouches *mapView = [[MapViewWithTouches alloc] initWithFrame:self.view.frame];
[self.view insertSubview:mapView atIndex:0];

Une idée de ce que je fais mal?

94
Martin

Le meilleur moyen que j’ai trouvé d’y parvenir est de reconnaître les gestes. D'autres méthodes impliquent beaucoup de programmation bidon qui duplique imparfaitement le code d'Apple, en particulier dans le cas du multitouch.

Voici ce que je fais: Mettre en œuvre un identificateur de geste qui ne peut pas être empêché et qui ne puisse empêcher d'autres identificateurs de geste. Ajoutez-le à l'affichage de la carte, puis utilisez les gestes de reconnaissance du début, du toucher, du déplacement, etc.

Comment détecter un tap à l'intérieur d'un MKMapView (sans astuces)

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
    }
}
144
gonzojive

Après une journée de pizzas, de cris, j'ai enfin trouvé la solution! Très propre!

Peter, j'ai utilisé votre astuce ci-dessus et l'ai légèrement modifié pour enfin avoir une solution qui fonctionne parfaitement avec MKMapView et devrait également fonctionner avec UIWebView

MKTouchAppDelegate.h

#import <UIKit/UIKit.h>
@class UIViewTouch;
@class MKMapView;

@interface MKTouchAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UIViewTouch *viewTouch;
    MKMapView *mapView;
}
@property (nonatomic, retain) UIViewTouch *viewTouch;
@property (nonatomic, retain) MKMapView *mapView;
@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

MKTouchAppDelegate.m

#import "MKTouchAppDelegate.h"
#import "UIViewTouch.h"
#import <MapKit/MapKit.h>

@implementation MKTouchAppDelegate

@synthesize window;
@synthesize viewTouch;
@synthesize mapView;


- (void)applicationDidFinishLaunching:(UIApplication *)application {

    //We create a view wich will catch Events as they occured and Log them in the Console
    viewTouch = [[UIViewTouch alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];

    //Next we create the MKMapView object, which will be added as a subview of viewTouch
    mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
    [viewTouch addSubview:mapView];

    //And we display everything!
    [window addSubview:viewTouch];
    [window makeKeyAndVisible];


}


- (void)dealloc {
    [window release];
    [super dealloc];
}


@end

UIViewTouch.h

#import <UIKit/UIKit.h>
@class UIView;

@interface UIViewTouch : UIView {
    UIView *viewTouched;
}
@property (nonatomic, retain) UIView * viewTouched;

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

@end

UIViewTouch.m

#import "UIViewTouch.h"
#import <MapKit/MapKit.h>

@implementation UIViewTouch
@synthesize viewTouched;

//The basic idea here is to intercept the view which is sent back as the firstresponder in hitTest.
//We keep it preciously in the property viewTouched and we return our view as the firstresponder.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"Hit Test");
    viewTouched = [super hitTest:point withEvent:event];
    return self;
}

//Then, when an event is fired, we log this one and then send it back to the viewTouched we kept, and voilà!!! :)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began");
    [viewTouched touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Moved");
    [viewTouched touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Ended");
    [viewTouched touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Cancelled");
}

@end

J'espère que cela aidera certains d'entre vous!

À votre santé

29
Martin
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleGesture:)];   
tgr.numberOfTapsRequired = 2;
tgr.numberOfTouchesRequired = 1;
[mapView addGestureRecognizer:tgr];
[tgr release];


- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state != UIGestureRecognizerStateEnded)
        return;

    CGPoint touchPoint = [gestureRecognizer locationInView:mapView];
    CLLocationCoordinate2D touchMapCoordinate = [mapView convertPoint:touchPoint toCoordinateFromView:mapView];

    //.............
}
21
iGo

Pour un MKMapView, la véritable solution consiste à reconnaître les gestes!

Moi, je voulais arrêter de mettre à jour le centre de la carte sur mon emplacement lorsque je fais glisser la carte ou que je pince pour zoomer.

Alors, créez et ajoutez votre outil de reconnaissance de mouvements à mapView:

- (void)viewDidLoad {

    ...

    // Add gesture recognizer for map hoding
    UILongPressGestureRecognizer *longPressGesture = [[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressAndPinchGesture:)] autorelease];
    longPressGesture.delegate = self;
    longPressGesture.minimumPressDuration = 0;  // In order to detect the map touching directly (Default was 0.5)
    [self.mapView addGestureRecognizer:longPressGesture];

    // Add gesture recognizer for map pinching
    UIPinchGestureRecognizer *pinchGesture = [[[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressAndPinchGesture:)] autorelease];
    pinchGesture.delegate = self;
    [self.mapView addGestureRecognizer:pinchGesture];

    // Add gesture recognizer for map dragging
    UIPanGestureRecognizer *panGesture = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)] autorelease];
    panGesture.delegate = self;
    panGesture.maximumNumberOfTouches = 1;  // In order to discard dragging when pinching
    [self.mapView addGestureRecognizer:panGesture];
}

Examinez la référence de classe UIGestureRecognizer pour voir tous les outils de reconnaissance de mouvements disponibles.

Comme nous avons défini le délégué à lui-même, nous devons implémenter le protocole UIGestureRecognizerDelegate:

typedef enum {
    MapModeStateFree,                    // Map is free
    MapModeStateGeolocalised,            // Map centred on our location
    MapModeStateGeolocalisedWithHeading  // Map centred on our location and oriented with the compass
} MapModeState;

@interface MapViewController : UIViewController <CLLocationManagerDelegate, UIGestureRecognizerDelegate> {
    MapModeState mapMode;
}

@property (nonatomic, retain) IBOutlet MKMapView *mapView;
...

Et substituez la méthode gestureRecognizer: gestureRecognizer shouldRecognizeSimultaneousWithGestureRecognizer: afin de permettre de reconnaître plusieurs gestes simultanément, si j'ai bien compris:

// Allow to recognize multiple gestures simultaneously (Implementation of the protocole UIGestureRecognizerDelegate)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

Maintenant, écrivez les méthodes qui seront appelées par nos identificateurs de gestes:

// On map holding or pinching pause localise and heading
- (void)handleLongPressAndPinchGesture:(UIGestureRecognizer *)sender {
    // Stop to localise and/or heading
    if (sender.state == UIGestureRecognizerStateBegan && mapMode != MapModeStateFree) {
        [locationManager stopUpdatingLocation];
        if (mapMode == MapModeStateGeolocalisedWithHeading) [locationManager stopUpdatingHeading];
    }
    // Restart to localise and/or heading
    if (sender.state == UIGestureRecognizerStateEnded && mapMode != MapModeStateFree) {
        [locationManager startUpdatingLocation];
        if (mapMode == MapModeStateGeolocalisedWithHeading) [locationManager startUpdatingHeading];
    }
}

// On dragging gesture put map in free mode
- (void)handlePanGesture:(UIGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateBegan && mapMode != MapModeStateFree) [self setMapInFreeModePushedBy:sender];
}
12
Joan

Juste au cas où quelqu'un essaie de faire la même chose que moi: je voulais créer une annotation au moment où l'utilisateur tape. Pour cela j'ai utilisé la solution UITapGestureRecognizer:

UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnMap:)];
[self.mapView addGestureRecognizer:tapGestureRecognizer];
[tapGestureRecognizer setDelegate:self];

- (void)didTapOnMap:(UITapGestureRecognizer *)gestureRecognizer
{
    CGPoint point = [gestureRecognizer locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
    .......
}

Cependant, didTapOnMap: a également été appelé lorsque j'ai tapé sur l'annotation et en a créé une nouvelle. La solution consiste à implémenter la UIGestureRecognizerDelegate:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if ([touch.view isKindOfClass:[MKAnnotationView class]])
    {
        return NO;
    }
    return YES;
}
5
jimpic

Vous aurez probablement besoin de superposer une vue transparente pour saisir les contacts, comme c'est souvent le cas avec les contrôles basés sur UIWebView. La vue Carte effectue déjà une série de tâches spéciales avec une touche tactile afin de permettre à la carte d'être déplacée, centrée, zoomée, etc., afin que les messages ne soient pas diffusés dans votre application.

Deux autres options (INATTENDUES) auxquelles je peux penser:

1) Renoncez au premier répondant via IB et réglez-le sur "Propriétaire du fichier" pour permettre au propriétaire du fichier de répondre aux touches. Je doute que cela fonctionne, car MKMapView étend NSObject, et non UIView, de sorte que les événements tactiles ne peuvent toujours pas être propagés jusqu'à vous.

2) Si vous souhaitez intercepter les changements d'état de la carte (par exemple sur un zoom), implémentez simplement le protocole MKMapViewDelegate pour écouter des événements particuliers. Mon intuition est que c’est votre meilleur moyen de capturer facilement certaines interactions (à moins de mettre en œuvre la vue transparente sur la carte). N'oubliez pas de définir le contrôleur de visualisation contenant MKMapView en tant que délégué de la carte (map.delegate = self).

Bonne chance.

3
MystikSpiral

Je n'ai pas expérimenté, mais il y a de bonnes chances que MapKit soit basé sur un cluster de classes. Par conséquent, le sous-classement est difficile et inefficace.

Je suggèrerais de faire de la vue MapKit une sous-vue d'une vue personnalisée, ce qui devrait vous permettre d'intercepter les événements tactiles avant qu'ils ne l'atteignent.

2
grahamparks

Ainsi, après une demi-journée de déconstruction, j’ai trouvé ce qui suit:

  1. Comme tout le monde a trouvé, pincer ne fonctionne pas. J'ai essayé les deux sous-classes MKMapView et la méthode décrite ci-dessus (en l'interceptant). Et le résultat est le même.
  2. Dans les vidéos sur l'iPhone de Stanford, un gars d'Apple dit que beaucoup de choses UIKit vont causer beaucoup d'erreurs si vous "transférez" les demandes tactiles (alias les deux méthodes décrites ci-dessus), et vous ne l'obtiendrez probablement pas travailler.

  3. LA SOLUTION: est décrite ici: Interception/Détournement d'événements iPhone Touch pour MKMapView . En gros, vous "attrapez" l'événement avant que tout intervenant ne le reçoive, et l'interprète ici.

1
thuang513

Dans Swift 3.0

import UIKit
import MapKit

class CoordinatesPickerViewController: UIViewController {

    @IBOutlet var mapView: MKMapView!
    override func viewDidLoad() {
        super.viewDidLoad()

        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(clickOnMap))
        mapView.addGestureRecognizer(tapGestureRecognizer)
    }

    @objc func clickOnMap(_ sender: UITapGestureRecognizer) {

        if sender.state != UIGestureRecognizerState.ended { return }
        let touchLocation = sender.location(in: mapView)
        let locationCoordinate = mapView.convert(touchLocation, toCoordinateFrom: mapView)
        print("Tapped at lat: \(locationCoordinate.latitude) long: \(locationCoordinate.longitude)")

    }

}
1
Francisco Castro

J'ai pris l'idée d'une vue transparente "superposée" dans la réponse de MystikSpiral et cela a parfaitement fonctionné pour ce que j'essayais de réaliser; solution rapide et propre.

En bref, j'avais un UITableViewCell personnalisé (conçu dans IB) avec un MKMapView à gauche et quelques UILabels à droite. Je voulais créer la cellule personnalisée afin que vous puissiez la toucher n'importe où, ce qui pousserait un nouveau contrôleur de vue. Cependant, le fait de toucher la carte n’a pas eu de répercussion sur la UITableViewCell jusqu’à ce que j’ajoute simplement une vue UIV de la même taille que la vue de la carte située au-dessus (dans IB) et que son arrière-plan soit la "couleur claire" dans le code ne pensez pas que vous pouvez définir clearColor dans IB ??):

dummyView.backgroundColor = [UIColor clearColor];

Je pensais que cela pourrait aider quelqu'un d'autre; certainement si vous voulez obtenir le même comportement pour une cellule de vue tableau.

0
petert

Merci pour la pizza et les cris - vous m'avez économisé beaucoup de temps.

multipletouchenabled fonctionnera de manière sporadique.

viewTouch.multipleTouchEnabled = TRUE;

En fin de compte, j'ai désactivé les vues lorsque je devais capturer le toucher (à un autre moment que celui nécessitant un affichage en pinchzoom):

    [mapView removeFromSuperview];
    [viewTouch addSubview:mapView];
    [self.view insertSubview:viewTouch atIndex:0];
0
BankStrong

Voici ce que j'ai mis ensemble, cela permet des zooms pincés dans le simulateur (je n'ai pas essayé sur un vrai iPhone), mais je pense que ce serait bien:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began %d", [touches count]);
 reportTrackingPoints = NO;
 startTrackingPoints = YES;
    [viewTouched touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
 if ([[event allTouches] count] == 2) {
  reportTrackingPoints = YES;
  if (startTrackingPoints == YES) {
   BOOL setA = NO;
   NSEnumerator *enumerator = [[event allTouches] objectEnumerator];
   id value;
   while ((value = [enumerator nextObject])) {
    if (! setA) {
     startPointA = [value locationInView:mapView];
     setA = YES;
    } else {
     startPointB = [value locationInView:mapView];
    }
   }
   startTrackingPoints = NO;
  } else {
   BOOL setA = NO;
   NSEnumerator *enumerator = [[event allTouches] objectEnumerator];
   id value;
   while ((value = [enumerator nextObject])) {
    if (! setA) {
     endPointA = [value locationInView:mapView];
     setA = YES;
    } else {
     endPointB = [value locationInView:mapView];
    }
   }
  }
 }
 //NSLog(@"Touch Moved %d", [[event allTouches] count]);
    [viewTouched touchesMoved:touches withEvent:event];
}

- (void) updateMapFromTrackingPoints {
 float startLenA = (startPointA.x - startPointB.x);
 float startLenB = (startPointA.y - startPointB.y);
 float len1 = sqrt((startLenA * startLenA) + (startLenB * startLenB));
 float endLenA = (endPointA.x - endPointB.x);
 float endLenB = (endPointA.y - endPointB.y);
 float len2 = sqrt((endLenA * endLenA) + (endLenB * endLenB));
 MKCoordinateRegion region = mapView.region;
 region.span.latitudeDelta = region.span.latitudeDelta * len1/len2;
 region.span.longitudeDelta = region.span.longitudeDelta * len1/len2;
 [mapView setRegion:region animated:YES];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 if (reportTrackingPoints) {
  [self updateMapFromTrackingPoints];
  reportTrackingPoints = NO;
 }


    [viewTouched touchesEnded:touches withEvent:event];
}

L'idée principale est que si l'utilisateur utilise deux doigts, vous suivez les valeurs. J'enregistre les points de départ et d'arrivée dans les points de départ A et B. Ensuite, j'enregistre les points de suivi actuels. Lorsque j'ai terminé, à l'aide de touchesEnded, je peux appeler une routine pour calculer les longueurs relatives de la ligne entre les points avec lesquels je commence , et la ligne entre le point que je termine en utilisant simple hypotenuse calc. Le rapport entre eux est le montant du zoom: je multiplie la région par ce montant.

J'espère que c'est utile pour quelqu'un. 

0
Dan Donaldson

Faites de MKMapView une sous-vue d’une vue personnalisée et implémentez-la

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

dans la vue personnalisée pour retourner soi-même au lieu de la sous-vue.

0
Peter N Lewis

Je remarque que vous pouvez suivre le nombre et l'emplacement des touches et obtenir l'emplacement de chacune dans une vue:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Moved %d", [[event allTouches] count]);

 NSEnumerator *enumerator = [touches objectEnumerator];
 id value;

 while ((value = [enumerator nextObject])) {
  NSLog(@"touch description %f", [value locationInView:mapView].x);
 }
    [viewTouched touchesMoved:touches withEvent:event];
}

Quelqu'un a-t-il déjà essayé d'utiliser ces valeurs pour mettre à jour le niveau de zoom de la carte? Il s'agirait d'enregistrer les positions de départ, puis les destinations d'arrivée, de calculer la différence relative et de mettre à jour la carte. 

Je joue avec le code de base fourni par Martin, et on dirait que cela fonctionnera ...

0
Dan Donaldson