25

Jan

iPhone: Custom gestures on your UIWebViews

Okay, so you saw Brandon's tutorial on creating a web browser using UIWebView. If you've played around with webviews enough, you've probably wanted to get some native gestures going on for your webpages. Let's say you even went so far as to subclass UIWebView to catch the touches methods and were unsuccessful. Here's why.

Let's pretend we have a horizontal bar that scrolls using javascript. You'll need to subclass UIWebView and add a subview to it that lays on top of the scrolling area. You'll catch the swipe gesture on the subview and execute some javascript to fire the scroll. I'll assume you already know how to add the subview. Then define the following methods:

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

These methods should be pretty clear on what they do. touchesMoved is only fired when your finger moves on the screen, but touchesBegan and touchesEnded are always fired when your finger interacts with the screen.

touchesMoved is the method we want to use for scrolling, as it's a swipe gesture. Simple enough, we just want to check that the finger moved at least a certain amount and then execute some javascript to make the area scroll.

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
	UITouch *touch = touches.anyObject;
	CGPoint currentTouchPosition = [touch locationInView:self];
	
	// If the swipe tracks correctly.
	if (fabsf(touchPositionStart.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN){
		// It appears to be a horizontal swipe.
		// Now find out which direction it went
		if (touchPositionStart.x < currentTouchPosition.x)
			// here's our jquery scrollable() plugin snippet
			[self stringByEvaluatingJavaScriptFromString:@"$('div.scrollable').scrollable().prevPage();"];
		else
			[self stringByEvaluatingJavaScriptFromString:@"$('div.scrollable').scrollable().nextPage();"];
	}
}
// I have the constants defined at the top of the source file, but here's what they look like
//#define HORIZ_SWIPE_DRAG_MIN 8

Now, this works great, but what if your scrollable area has links? Then it gets more difficult. We'll have to pass the touches through if they aren't a swipe. If you've tried this before the real question is "Where do you pass them?", as it is not the UIWebView itself that handles the touches, it's a subview of a subview of the UIWebView.

Add a BOOL ivar to the class and call it passToWebView. We'll us this in the touchesBegan and touchesEnded methods to decide whether to pass the events. In your init method (I'm using initWithFrame) make sure you set it to YES. Then in the touchesMoved method, make sure you set it to NO inside the swipe found conditional.

Now add the following to your touchesBegan and touchesMoved methods:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
	UITouch *touch = [touches anyObject];
	touchPositionStart = [touch locationInView:self];

	if(passToWebView && webDocumentView){
		[webDocumentView touchesBegan:touches withEvent:event];
	}
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
	if(passToWebView && webDocumentView){
		[webDocumentView touchesEnded:touches withEvent:event];
	}
}

And make sure you add an else to the touchesMoved so it gets reset to YES

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
	UITouch *touch = touches.anyObject;
	CGPoint currentTouchPosition = [touch locationInView:self];
	
	// If the swipe tracks correctly.
	if (fabsf(touchPositionStart.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN){
		passToWebView = NO;
		// It appears to be a swipe.
		if (touchPositionStart.x < currentTouchPosition.x)
			[self stringByEvaluatingJavaScriptFromString:@"$('div.scrollable').scrollable().prevPage();"];
		else
			[self stringByEvaluatingJavaScriptFromString:@"$('div.scrollable').scrollable().nextPage();"];
	}else{
		// default handling
		passToWebView = YES;
	}
}

These methods should be pretty clear, all except for webDocumentView. This is the subview of a subview I mentioned earlier. It's an undocumented class and it's hidden down in there, so you're best off defining a function to find it and save it in an ivar (make sure you define the @property as (nonatomic,copy) not (nonatomic,retain) for good measure. You'll want to have this webDocumentView available from the start, so call this method from your init method.

- (UIView *)findUIWebDocumentView{
	UIView *webDocumentView = nil;
	NSString *className;

	// The UIScrollView is the first subview
	UIView *scroller = [[self subviews] objectAtIndex:0];

	// Who knows if it will remain where it is now, so best to be safe and loop over
	// all the subviews comparing the class names
	for(UIView *view in [scroller subviews]){
		className = [NSString stringWithFormat:@"%@", [view class]];
		if([className isEqualToString:@"UIWebDocumentView"]){ 
			NSLog(@"Found the UIWebDocumentView");
			webDocumentView = view;
		}
	}
	return webDocumentView;
}

Here's my UIWebView subclass in it's entirety:


#import "MyWebView.h"

#define HORIZ_SWIPE_DRAG_MIN 8

@implementation MyWebView

@synthesize scrollerView, webDocumentView;

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
		[self setMultipleTouchEnabled:YES];
		
		scrollerView = [[UIView alloc] initWithFrame:CGRectMake(0,320,320,64)];
		[self addSubview:scrollerView];
		[scrollerView release];
		passToWebView = YES;
		webDocumentView = [self findUIWebDocumentView];
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
	UITouch *touch = [touches anyObject];
	touchPositionStart = [touch locationInView:self];

	if(passToWebView && webDocumentView){
		[webDocumentView touchesBegan:touches withEvent:event];
	}
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
	if(passToWebView && webDocumentView){
		[webDocumentView touchesEnded:touches withEvent:event];
	}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
	UITouch *touch = touches.anyObject;
	CGPoint currentTouchPosition = [touch locationInView:self];
	
	// If the swipe tracks correctly.
	if (fabsf(touchPositionStart.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN){
		passToWebView = NO;
		// It appears to be a swipe.
		if (touchPositionStart.x < currentTouchPosition.x)
			[self stringByEvaluatingJavaScriptFromString:@"$('div.scrollable').scrollable().prevPage();"];
		else
			[self stringByEvaluatingJavaScriptFromString:@"$('div.scrollable').scrollable().nextPage();"];
	}else{
		// default handling
		passToWebView = YES;
	}
}

- (UIView *)findUIWebDocumentView{
	UIView *documentView = nil;
	NSString *className;
	UIView *scroller = [[self subviews] objectAtIndex:0];
	for(UIView *view in [scroller subviews]){
		className = [NSString stringWithFormat:@"%@", [view class]];
		NSLog(@"found a %@", className);
		if([className isEqualToString:@"UIWebDocumentView"]){ 
			NSLog(@"found the UIWebDocumentView");
			documentView = view;
		}
	}
	return documentView;
}

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

@end