Swiping and pinching with UIWebView

One of the most versatile classes that the iOS SDK offers is UIWebView. Indeed, it can be used as a very flexible tool to display a vast range of content types: rich text, graphics in SVG format, PDF files, and so on. It is also, IMO, the cornerstone of any sane approach to supporting multiple mobile platforms, e.g. iOS and Android. Inevitably, when you integrate UIWebView into your app, you come to face a problem: how you handle pinching and swiping correctly. As flexible as it is, in fact, UIWebView has got a major shortcoming in its “greediness” to touches: it wants them all and it handles it its own way, i.e., like a web browser would do. This is not what you want in most cases and this post will outline an approach to make UIWebView behave like any other views under iOS.

First of all, a clarification. You could implement your iOS app as a full HTML5 app and then load it into a UIWebView instance, almost forgetting about the underlying platform and relying for everything on HTML 5 capabilities. In that case, you would handle touches directly in your javascript, with a wise combination of CSS3 magics. But it is not this I am talking about.

What I am talking is this: imagine an app where you show a list of tweets, fully formatted tweets; this would be very appropriate to display by loading some HTML in UIWebView; what, then, if you also want to be able to swipe laterally that list of tweets, so that it uncovers some settings panel underneath? Or imagine the new Facebook app for iOS, where you can slide sideways your wall to uncover another list of options. It is not important what the underlying view is, it can even be another UIWebView. The nice point of the approach I am going to describe is that you can mix UIWebView instances and other view types (table views, control views, core data based views, you name it) and still manage touches in a consistent way across all of them.

About UIWebView greediness and a solution to it

You know, the problem with UIWebView is that it contains a UIScrollView (and this one, in turn, contains a real web kit view); actually, it is UIScrollView that is touch-greedy. The problem here lies with the fact that sometimes you would like to be able to get touches before they ever get to your UIScrollView; you would like to process them according to your app logics and then, possibly, dispatch them further, or simply suppress them.

Wrapping your UIWebView in another view and using gesture recognizers will not work (See here and here).

So, here is the basic approach:

  1. you subclass UIWindow and install it into your app;
    @interface SDSWebWindow : UIWindow {
    ....
    }
    ...
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    self.window = [[SDSWebWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    ...
    }
    
  2. in your custom UIWindow, you override - (void)sendEvent:(UIEvent*)eventto make it handle touches the way you like
    - (void)sendEvent:(UIEvent*)event {
    NSSet* allTouches = [event allTouches];
    UITouch* touch = [allTouches anyObject];
    UIView* touchView = [touch view];
    ...
    }
    
  3. you register with you custom UIWindow the views which you want to reroute touches for:
    @interface SDSWebWindow : TTNavigatorWindow {
    ...
    }
    @property (nonatomic, retain) NSMutableArray* controlledViews;
    
    ...
    
    //-- by executing this, you are registering a view with the custom window
    [(SDSWebWindow*)[UIApplication sharedApplication].delegate.window addObject:myView];
    
    

Now, going into more detail about sendEvent, it can have the usual structure:

if (touch.phase == UITouchPhaseBegan) {
} else if (touch.phase == UITouchPhaseMoved) {
} else if (touch.phase == UITouchPhaseEnded) {
} else if (touch.phase == UITouchPhaseCancelled) {
}

where you do your magic in order to detect taps, swipes, pinches, and more complex gestures. One very important things to notice at this respect is that UIScrollView has a somewhat nasty behavior in that it sorts of “resets” the touch.view after a few calls into UITouchPhaseMoved. This is possibly what makes UIScrollView so touch-unfriendly, to say it in a way. One way to provide a workaround is to save in an ivar the initialView that received the touch, so that when touch.view is made nil by UIScrollView, you can restore it. This is done like this:

[sourcecode]
if (!touchView && _initialView && touch.phase != UITouchPhaseBegan)
touchView = _initialView;
….
if (touch.phase == UITouchPhaseBegan) {
… _initialView = touchView;
}
[\sourcecode]

Once you have this in grip, the only other aspect to discuss is how you actually handle the gestures after you have recognized them. You could define some delegate and register the delegate instead of the view; you could even think of registering UIGestureRecognizers with the SDSWebWindow. But one simpler way is just dispatch a notification to a general handler and let it do its job. Since this is quite trivial to do, I am not going into further detail about it.

This much as to how to tackle and solve the problem of making UIWebView a well-behaved citizen of your app along with the many other views you have there. If you are interested in the nitty-gritty details, I suggest you to have a look at the full code hosted on github. It refers a customization of TTNavigatorWindow, but if you derive from UIWindow instead of TTNavigatorWindow and simply throw away all the references to Three20 headers, you’ll have a pretty good base to start.

Copyright © Labs Ramblings

Built on Notes Blog by TDH
Powered by WordPress

That's it - back to the top of page!