如何拦截MKMapView或UIWebView对象的触摸事件?

97

我不确定自己做错了什么,但我尝试在一个 MKMapView 对象上捕捉触摸事件。我通过创建以下类来对其进行子类化:

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

@interface MapViewWithTouches : MKMapView {

}

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

@end

实现代码为:

#import "MapViewWithTouches.h"
@implementation MapViewWithTouches

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

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

}
@end

但是当我使用这个类时,控制台上似乎什么也没有显示:

MapViewWithTouches *mapView = [[MapViewWithTouches alloc] initWithFrame:self.view.frame];
[self.view insertSubview:mapView atIndex:0];
任何想法我做错了什么?
15个回答

149
我发现最好的方法是使用手势识别器。其他方式往往需要进行很多hackish编程以不完全复制苹果的代码,特别是在多点触控的情况下。
这是我的做法:实现一个无法被阻止且不能阻止其他手势识别器的手势识别器。将其添加到地图视图中,然后使用手势识别器的touchesBegan、touchesMoved等来自如你所愿。
如何检测MKMapView内的任何点击(无需技巧)
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
    }
}

3
“lockedOnUserLocation”是用来做什么的? - jowie
1
这在大部分情况下都很有效,但我发现其中一个问题。如果您的 Web 视图中包含带有控件的 HTML5 video 标签,则手势识别器将防止用户使用控件。我一直在寻找解决此问题的方法,但尚未找到。 - Bryan Irace
同样适用于Web视图,效果非常好。 - AndyDunn
@gonzojive:这个例子对立即响应触摸非常有效,但是否有一种方法使地图视图响应更类似于“触摸抬起”事件? - markdorison
尝试使用此方法将发生在地图视图顶部的触摸事件传递回地图视图。但是没有成功。是否可能在SuperView中拦截手势,然后将其传递给所有子视图,并让所有子视图执行某些操作? - lostintranslation
显示剩余10条评论

29

经过一天的比萨和尖叫声,我终于找到了解决方案!非常棒!

Peter,我使用了你上面的技巧并稍微修改了一下,最终得到了一个在MKMapView上完美运行,并且应该也可以在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

我希望这可以对你们中的一些人有所帮助!

干杯!


14
好的。小建议:你应该避免使用“UI”作为你自己编写类的前缀。苹果公司保留或不鼓励使用“NS”或“UI”作为类前缀,因为这些前缀可能会与苹果公司的类冲突(即使是私有类)。 - Daniel Dickison
嘿,丹尼尔,你说得完全正确,我也是这么想的!为了补充我的上面的答案,让我加一个小警告:我的示例假设只有一个对象viewTouched在消耗所有事件。但事实并非如此。您可能会在地图顶部放置一些注释,那么我的代码就不再起作用了。要100%工作,您需要记住每个hitTest与特定事件相关联的视图(并在触摸结束或取消触摸时释放它,以便您无需跟踪已完成的事件...)。 - Martin
1
非常有用的代码,谢谢Martin!我想知道在实现这个之后你是否尝试过捏合缩放地图?对我来说,当我使用基本上与你上面相同的代码使其工作时,除了捏合缩放地图外,似乎一切都正常。有人有什么想法吗? - Adam Alexander
实际上,这段代码:
  • (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Map touch-began count: %d", [touches count]); [viewTouched touchesBegan:touches withEvent:event]; }
  • (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Touch Moved: %d", [touches count]); [viewTouched touchesMoved:touches withEvent:event]; }
在尝试捏合/缩放时会产生以下日志输出: Map touch-began count: 1 Touch Moved: 1 Touch Moved: 1[等等,省略]需要帮助吗?
- Olie
(哎呀!那个不太好。希望有编辑权限的人能够修复它。) - Olie
显示剩余7条评论

24
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];

    //.............
}

3
我不确定为什么这不是最佳答案。看起来完美运作且更简单。 - elsurudo

12

对于MKMapView,真正奏效的解决方案是使用手势识别!

我想要在拖动地图或捏合缩放时停止更新以我的位置为中心的地图。

因此,创建并将您的手势识别器添加到地图视图中:

- (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];
}

查看UIGestureRecognizer Class Reference,以查看所有可用的手势识别器。

因为我们将委托定义为self,所以我们必须实现协议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;
...

我想要覆盖方法gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:,以便允许同时识别多个手势,如果我理解正确的话:

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

现在编写我们的手势识别器将调用的方法:

// 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];
}

这个解决方案非常完美!这里有一些快捷方式:如果你想要拦截用户结束任何操作时使用,这应该就足够了 - (void)handleLongPressAndPinchGesture:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { NSLog(@"handleLongPressAndPinchGesture Ended"); } } - Alejandro Luengo
还要不要忘记添加委托<UIGestureRecognizerDelegate>。 - Alejandro Luengo

8

如果有人想做和我一样的事情:我想在用户点击的位置创建一个注释。为此,我使用了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];
    .......
}

然而,当我点击标注并创建新标注时,也会调用didTapOnMap:。 解决方案是实现 UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if ([touch.view isKindOfClass:[MKAnnotationView class]])
    {
        return NO;
    }
    return YES;
}

这是一个很好的解决方案!但是,如果您使用自定义视图作为MKAnnotation,它将无法正常工作。在这种情况下,您可能会发现另一个注释的子视图触发了手势识别器。我不得不递归检查touch.view的superview,以找到潜在的MKAnnotationView。 - KIDdAe

3
您可能需要叠加一个透明视图来捕获触摸,就像经常使用基于UIWebView的控件一样。地图视图已经对触摸进行了一些特殊处理,以允许地图移动、居中、缩放等操作,因此消息没有传递到您的应用程序。
我可以想到另外两个(未经测试的)选项:
1)通过IB重新签署第一个响应者,并将其设置为“文件所有者”,以允许文件所有者响应触摸。我怀疑这不会起作用,因为MKMapView扩展NSObject而不是UIView,因此触摸事件仍然可能无法传播到您。
2)如果您想要在地图状态更改时捕获(例如缩放),只需实现MKMapViewDelegate协议以侦听特定事件。我猜这是您最容易捕获某些交互的最佳方法(除了在地图上实现透明视图)。不要忘记将包含MKMapView的视图控制器设置为地图的委托(map.delegate = self)。
祝你好运。

MKMapView 绝对是 UIView 的子类。 - Daniel Dickison

2

在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)")

    }

}

2

经过半天的折腾,我找到了下面的解决方法:

  1. 像其他人一样,捏合手势不起作用。我试过子类化MKMapView和上述方法(拦截),结果都一样。
  2. 在斯坦福的iPhone视频中,一位来自苹果公司的人说,如果你“转移”触摸请求(也就是上述两种方法),许多UIKit的东西会导致很多错误,并且你可能无法使其工作。

  3. 解决方案: 在这里描述: 拦截/劫持MKMapView的iPhone触摸事件。基本上,你需要在任何响应者得到它之前就“捕捉”事件并在那里解释它。


2

我没有进行过实验,但 MapKit 很可能是基于类簇的,因此对其进行子类化可能会很困难且无效。

我建议将 MapKit 视图作为自定义视图的子视图,这样可以在触摸事件到达它之前拦截它们。


你好,Graham!感谢你的帮助!如果我像你建议的那样创建一个超级自定义视图,我该如何将事件转发到MKMapView?有什么想法吗? - Martin

0

我注意到您可以跟踪触摸的数量和位置,并获取视图中每个触摸的位置:

- (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];
}

有其他人尝试使用这些值来更新地图的缩放级别吗?这将是记录起始位置,然后是完成位置,计算相对差异并更新地图的问题。

我正在使用Martin提供的基本代码,并且看起来它将起作用...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接