Blog

bevkjbvkbdkdxoeoziejdoiehz fiugebfuyegwik

WebInspector on iOS

A couple of weeks ago, after seeing SteveStreza's tweet on WebView+Debug, I started digging into class dumps of the iOS WebKit framework to find out what else is there for debugging. I stumbled upon Nathan de Vries' post on remote WebInspector, but trying it out revealed that it doesn't work on the device at all.

What does WebKit framework offer us to find out what's going on?

[NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];
id sharedServer = [NSClassFromString(@"WebView") performSelector:@selector(sharedWebInspectorServer)];

In the simulator, this gives us a WebInspectorServerHTTP, but on the device it is a WebInspectorServerXPC. We can only speculate here, but it seems reasonable to assume that Apple uses XPC so that no server socket is opened by the WebKit framework on the device, but rather wants to have a separate process to handle the HTTP connection. As this is all private API, we have no idea how to spawn that service or if it is even implemented yet, we need to find another way.

It turns out that the WebInspectorServer base class defines a start class method. Let's try this little snippet:

id newServer = [[NSClassFromString(@"WebInspectorServerHTTP") alloc] init];
[newServer performSelector:NSSelectorFromString(@"start")];

Obviously, we need to hold on to that instance for the server to stick around, so be careful with that in an ARC environment - that bit me in a UIWebView category during testing. With that, we have now started the HTTP WebInspector server, but there still isn't anything listening to port 9999 when we do a port scan of the iPhone. Unfortunately, it turns out that the port is only opened locally.

My next idea was setting up a proxy on the device which forwards the requests to the local WebInspector server. For that I used the GTMHTTPServer class from Google Toolbox for Mac. I won't bore you with the implementation here and unfortunately, this didn't work out, either. I could access the list of open pages just fine, but the details page didn't work at all through the proxy, but I couldn't figure out why. All the JavaScript and CSS files were loaded fine. I would appreciate any ideas on what went wrong with this approach.

Finally, I settled for writing an app with two web views, one for the actual page and one for WebInspector. Switching between them would be done using swipe gestures, so I had to dig into how to add gestures on top of a UIWebView, which isn't as difficult as one would think. For that, we are going to do what Apple does recommend and subclass UIWebView. In this case, we are probably OK, though - we are building a private app for testing websites, so in the worst case we have to test our websites the old way again if something breaks.

Just attaching a gesture recognizer on the web view itself doesn't work. From what I gathered, the issue seems to be that there's a custom hitTest:withEvent: implementation at play here. The actual subview which handles touches for the web view is UIWebDocumentView, so we first need to find that in the hierarchy:

-(UIView*)documentView {
    for (UIView* view in self.subviews) {
        if ([view isKindOfClass:[UIScrollView class]]) {
            for (UIView* subview in view.subviews) {
                if ([subview isKindOfClass:NSClassFromString(@"UIWebDocumentView")]) {
                    return subview;
                }
            }
        }
    }
    return nil;
}

Now we are building a custom initWithFrame: which attaches our gesture recognizers:

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setMultipleTouchEnabled:YES];

        UISwipeGestureRecognizer* swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft)];
        swipe.direction = UISwipeGestureRecognizerDirectionLeft;
        [self.documentView addGestureRecognizer:swipe];

        swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight)];
        swipe.direction = UISwipeGestureRecognizerDirectionRight;
        [self.documentView addGestureRecognizer:swipe];
    }
    return self;
}

With that, we can now do whatever we want in swipeLeft and swipeRight. This worked fine for me, but it will probably break if the webpage itself uses gesture recognition. You have to see for yourself. Another idea would be to utilize shake for switching or use a UIToolbar. I wanted to avoid the toolbar to not lose any screen estate.

To conclude, you can get the final app on Github. None of it is App Store safe of course, because of all the private API that is used. That doesn't keep you from utilizing the app to debug your mobile web sites, though. In a follow-up post, I will describe the techniques I used to discover private API, as it sometimes can turn out to be quite useful and the information needs to be scraped together in bits and pieces at the moment.