How do I capture touch events such as - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event without subclassing a UIView nor using UIViewControllers.
What happens is that I have a simple UIView created programmatically and I need to detect basic tap events.
If you are writing your app for iOS 4, use UIGestureRecognizer. You can then do what you want. Recognize gestures without subclassing.
Otherwise, subclassing is the way to go.
There's just no reason not to. If you subclass and add nothing it's just a UIView called by another name. All you are doing is intercepting those functions that you are interested in. Don't forget you can do [super touchesBegan:touches] inside your subclass' touchesBegan if you don't want to stop responders up the chain from getting those events too.
I don't why you don't want to use the normal method of subclassing a UIView to capture touch events, but if you really need to do something weird or sneaky, you can capture all events (including touch events) before they get sent down the view hierarchy by trapping/handling the sendEvent: method at the UIWindow level.
CustomGestureRecognizer.h
#import <UIKit/UIKit.h>
#interface CustomGestureRecognizer : UIGestureRecognizer
{
}
- (id)initWithTarget:(id)target;
#end
CustomGestureRecognizer.mm
#import "CustomGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface CustomGestureRecognizer()
{
}
#property (nonatomic, assign) id target;
#end
#implementation CustomGestureRecognizer
- (id)initWithTarget:(id)target
{
if (self = [super initWithTarget:target action:Nil]) {
self.target = target;
}
return self;
}
- (void)reset
{
[super reset];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self.target touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
[self.target touchesMoved:touches withEvent:event];
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent: event];
[self.target touchesEnded:touches withEvent:event];
}
#end
Usage:
CustomGestureRecognizer *customGestureRecognizer = [[CustomGestureRecognizer alloc] initWithTarget:self];
[glView addGestureRecognizer:customGestureRecognizer];
Related
I'm creating custom gesture recognizer. The problem is that reset method is never called so I can't reset the state of recognizer. As result it works only for the first time
#implementation TouchGestureRecognizer {
UIGestureRecognizerState mState;
}
-(UIGestureRecognizerState) state {
return mState;
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if( [touches count] == 1 ) {
mState = UIGestureRecognizerStateBegan;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if( [touches count] == 1 ) {
mState = UIGestureRecognizerStateChanged;
}
}
- (void)reset {
mState = UIGestureRecognizerStatePossible;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
mState = UIGestureRecognizerStateRecognized;
}
#end
The documentation states:
The runtime calls this method after the gesture-recognizer state has been set to UIGestureRecognizerStateEnded or UIGestureRecognizerStateRecognized.
It seems that is what you are doing in touchesEnded:. Put a breakpoint in this method and take it from there.
You have to write this in your .h file.
#import <UIKit/UIGestureRecognizerSubclass.h>
I was trying to find a way to recognise a touch&hold on my buttons. I thought that to subclass my buttons was a good idea, but I'm now struggling with the whole idea of subclasses, parentsviews and the viewcontroller. So please forgive, I fear that this is a beginner's question:
How do I call a method (which I've defined in my ViewController) from a subclassed UIButton?
[self someMethod]; doesn't work - as UIButton is not a descendent of my ViewController.
[super someMethod]; doesn't work either - same problem I suppose
[self.superview someMethod]; ... again no luck
[MyViewController someMethod]; doesn't work either -- as it is 'undecleared' -- do I have to import my ViewController? Or do some kind of protocol/class call?
Any help would be very much appreciated.
Here is my subclass:
//
// MoleButton.h
//
#import <Foundation/Foundation.h>
#interface MoleButton : UIButton {
int page;
NSString *colour;
UIViewController *theViewController;
NSTimer *holdTimer;
}
#property (nonatomic, assign) int page;
#property (nonatomic, assign) NSString *colour;
#end
//
// MoleButton.m
#import "MoleButton.h"
#implementation MoleButton
#synthesize page, colour;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self.superview touchesBegan:touches withEvent:event];
[holdTimer invalidate];
holdTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(touchWasHeld) userInfo:nil repeats:NO];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self.superview touchesMoved:touches withEvent:event];
[holdTimer invalidate];
holdTimer = nil;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self.superview touchesEnded:touches withEvent:event];
}
- (void)touchWasHeld
{
holdTimer = nil;
// do your "held" behavior here
NSLog(#"TOUCH WAS HELD!!!!");
[self.theViewController doSomething];
}
#end
You can simply add a property in the subclassed UIButton class, which holds the view controller. When initializing it, you need to add the controller, for sure.
Use the very simple delegate concept of Objective-C .
Check my answer in the below post for using delegate in Objective-C .
How do I set up a simple delegate to communicate between two view controllers?
I am working to make my iPhone app compatible for the iPad. The user can tap or swipe the screen to activate certain functions, and it works fine on my iPhone version. There is a UIScrollView on the page which I have subclassed to make it "swipeable," i.e. it passes up all of its touch functions to its superview as such:
#implementation SwipeableScrollView
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self.superview touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self.superview touchesMoved:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self.superview touchesEnded:touches withEvent:event];
}
#end
This works fine on the iPhone version, passing both taps and swipe gestures, but on the iPad, I get the following strange behavior:
Taps are passed to the superview properly.
But, swipe gestures are not passed at all.
Any idea why this would be working on the iPhone but not the iPad?
The problem is, that UIScrollView on the iPad cancels content touches very fast, even if canCancelContentTouches is set to NO. Also, overwriting -touchesShouldCancelInContentView: does not help. Read more here: link text
While overriding touch events was necessary on the iPhone, for the iPad Apple has introduced UIGestureRecognizers that make tasks like this much more straightforward and easy to implement. You will probably need to refactor your code to use them.
you can make custom uiscrollView and implements it's delegate so you can do what you want with scrollview it works with me fine
#import <UIKit/UIKit.h>
#protocol ScrollingDelegate <NSObject>
#required
- (void)scrolltouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)scrolltouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)scrolltouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
#end
Implementation
#interface CustomScrollView : UIScrollView
{
id <ScrollingDelegate> delegate;
}
#property (nonatomic,strong) id scrollDelegate;
#end
#import "CustomScrollView.h"
#implementation CustomScrollView
#synthesize scrollDelegate;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self=[super initWithCoder:aDecoder];
if (self)
{
}
return self;
}
-(BOOL)touchesShouldCancelInContentView:(UIView *)view
{
return NO;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.scrollDelegate scrolltouchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.scrollDelegate scrolltouchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.scrollDelegate scrolltouchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
}
#end
this is my program .
//Interface file.
#import <Foundation/Foundation.h>
#interface reportView : UIView {
}
- (void)touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event;
- (void)touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event;
- (void)touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event;
#end
//ImplementationFile.
#import "reportView.h"
#implementation reportView
- (void) touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event{
[self.nextResponder manageTouches:touches];
}
- (void) touchesEnded: (NSSet *)touches withEvent: (UIEvent *) event{
[self.nextResponder manageTouches:touches];
}
- (void) touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event{
[self.nextResponder manageTouches:touches];
}
#end
I am getting the warning.
warning:'UIResponder' may not respond to '-manageTouches:'
I got 3 warnings as above.
I am getting the needed output correctly but how to resolve the warnings.
First, since your code looks exactly like listing 14.2 from iPhone in Action, did you remember to copy the code for -manageTouches: from listing 14.3 into your class?
Assuming that you did, the issue is that the nextResponder property points to an instance of UIResponder, which could be just about anything (your class, a window, some other OS-managed thing....), and the compiler doesn't know for sure that this particular responder implements a method -manageTouches:. Your options are basically to either live with the warning, or cast the value of nextResponder to your class. (FWIW, the bottom of page 247 mentions this.)
For that matter, you don't really know, at runtime, that the next responder will respond to that message. To be on the safe side, you might want to use -respondsToSelector: or -isKindOfClass: to validate that assumption.
Is there a bug in the 3.0 SDK that disables real-time zooming and intercepting the zoom-in gesture for the MKMapView? I have some real simple code so I can detect tap events, but there are two problems:
zoom-in gesture is always interpreted as a zoom-out
none of the zoom gestures update the Map's view in realtime.
In hitTest, if I return the "map" view, the MKMapView functionality works great, but I don't get the opportunity to intercept the events.
Any ideas?
MyMapView.h:
#interface MyMapView : MKMapView
{
UIView *map;
}
MyMapView.m:
- (id)initWithFrame:(CGRect)frame
{
if (![super initWithFrame:frame])
return nil;
self.multipleTouchEnabled = true;
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(#"Hit Test");
map = [super hitTest:point withEvent:event];
return self;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"%s", __FUNCTION__);
[map touchesCancelled:touches withEvent:event];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event
{
NSLog(#"%s", __FUNCTION__);
[map touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
NSLog(#"%s, %x", __FUNCTION__, mViewTouched);
[map touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
NSLog(#"%s, %x", __FUNCTION__, mViewTouched);
[map touchesEnded:touches withEvent:event];
}
The best way I have found to achieve this is with a Gesture Recognizer. It's unclear if you want to recognize zoom events yourself or just detect when the user is zooming/panning.
I don't need to detect a map pan or zoom--i just care if the user has put a finger down anywhere in the MKMapView. If you need to detect zooming or panning with 100% recall and precision, it might be more complicated than this. What we really need is an open source implementation of MKMapView so we can add this to the delegate, among many other features.
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 deal with the relevant touch events as you desire.
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
I had the same problem - I wanted to draw map scales on top of Map View. In order to do it I had to intercept the touch events, and then send them back to the Map View. Unfortunately, when the MKMapView isn't the original receiver of the events, some smooth panning and zooming animations are not working any more.
However I have found a solution to this problem - a bit hacky but works:
1. I have put my MapScales UIView on top of MKMapView, and turned off receiving events in it, so that underlying MKMapView received the events by default.
2. I have subclassed UIWindow with MyMainWindow class and in it I have overriden the method:
- (void) sendEvent:(UIEvent*)event {
[super sendEvent:event];
[self send_the_event_also_to_my_MapScales_component_with_use_of_listener_design_pattern];
}
I have made the main window of my application an instasnce of MyMainWindow.
In this way my MapScales component receives and can react to all the touch events, and at the same time it is not spoiling the underlying MKMapView :)
#import <UIKit/UIKit.h>
#import "UIViewTouch.h"
#import <MapKit/MapKit.h>
#interface MapTouchAppDelegate : NSObject <UIApplicationDelegate>
{
UIViewTouch *viewTouch;
MKMapView *mapView;
UIWindow *window;
}
#property (nonatomic, retain) IBOutlet UIWindow *window;
#property (nonatomic, retain) UIViewTouch *viewTouch;
#property (nonatomic, retain) MKMapView *mapView;
#end
#import "MapTouchAppDelegate.h"
#implementation MapTouchAppDelegate
#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];
// Override point for customization after application launch
[window makeKeyAndVisible];
}
- (void)dealloc {
[window release];
[super dealloc];
}
#import <UIKit/UIKit.h>
#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
#import "UIViewTouch.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");
self.multipleTouchEnabled = true;
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
This code will detect touches as well as zooming.
Try
[super touchesEnded:touches withEvent:event];
instead of
[map touchesEnded:touches withEvent:event];
And apply this idea to all of the touch event methods. That way, the touches should travel down the responder chain, and peace will be restored.
To extend up on #chomasek's answer, in order to only process those touches for the map view, I do the following:
Give the map view a tag, like 99
When I get touch, traverse up the view hierarchy of the receiving view, looking for a view with the above tag.
Here is the code:
// this is in the view controller containing the map view
// kICTagForMapView is just a constant
_mapView.tag = kICTagForMapView;
Then in sendEvent:
// this does into the UIWindow subclass
BOOL isMapView = NO;
UIView* superView = touch.view.superview;
while(superView)
{
//Debug(#"superView = %#", superView);
if (superView.tag == kICTagForMapView)
{
isMapView = YES;
break;
}
superView = superView.superview;
}
if (isMapView == NO) return;
// do stuff here
MKMapView does not respond to the touch methods listed above...