Fun with UIWebView

Introduction

Hi. This is my first post for iDevBlogADay so a quick introduction is in order.

My name is Wiley Wimberly and I am a part-time indie iOS developer in Greenville, SC. I use the studio name Warm Fuzzy Apps for my indie projects. During the day I write glue code to make back end systems play nicely together for a telecommunications company.

I have one released app, RipDeck, and several others in various stages of completion. RipDeck is a fitness app based on the classic deck of cards workout. Give it a try for a fast paced, fun and challenging workout.

Background

While working on an update to RipDeck, I started investigating the use of web technologies within web views to simplify the development of certain features. I already use a UIWebView in several places to display static HTML content and I wanted to see if adding JavaScript to the mix was feasible.

I have looked into web app wrappers like PhoneGap, but they seem to be overkill for what I am trying to do. I simply want to keep my current Objective-C base but utilize JavaScript, CSS, and HTML within web views for some features.

I’m sure the Objective-C purists out there are cringing over this idea, but please bear with me.

Required Functionality

I need to be able to accomplish the following tasks in order to make using JavaScript work as an alternative to implementing everything in Objective-C.

  • Call JavaScript from Objective-C
  • Call Objective-C from JavaScript
  • Log debug statements from JavaScript

Seems pretty straightforward.

Calling JavaScript from Objective-C

Calling a JavaScript function in a page loaded inside a web view from Objective-C is done with the stringByEvaluatingJavaScriptFromString: method of UIWebView.

NSString *js = @"document.getElementById('output').innerHTML = 'hello from Objective-C';";
NSString *res = [webView stringByEvaluatingJavaScriptFromString:js];
NSLog(@"%@", res);


Easy peasy.

Be sure to read the discussion in the UIWebView documentation about the limits of executing JavaScript code with stringByEvaluatingJavaScriptFromString:. If you keep the call under 10 seconds and under 10 MB you should be ok.

Calling Objective-C from JavaScript

After finding out how simple it is to call JavaScript from Objective-C, I was a bit troubled to find that it isn’t quite as easy to do the reverse and call Objective-C from JavaScript.

Digging around in the docs I stumbled upon an article titled Calling Objective-C Methods From JavaScript. Bingo! That looks like just what I need. No such luck. Unfortunately, it is for OS X and there does’t seem to be a documented iOS equivalent to WebScriptObject.

What we do have is shouldStartLoadWithRequest: which is part of the UIWebViewDelegate Protocol and can be used to capture carefully crafted page requests and call Objective-C code that way. Not very pretty but it mostly works.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  NSURL *url = [request URL];
  if ([[url scheme] isEqualToString:@"js2objc"]) {
    // remove leading / from path    
    [self helloFromJavaScript:[[url path] substringFromIndex:1]];
    return NO; // prevent request
  } else {
    return YES; // allow request
  }
}

The JavaScript side of the equation looks like this:

document.getElementById('hello').onclick = function () {
  var msg = 'hello from JavaScript';
  document.getElementById('output').innerHTML = msg;
  document.location = 'js2objc:///' + encodeURIComponent(msg);
}

The key here is to encode arguments in the url and then change the document.location to trigger a request from JavaScript. The shouldStartLoadWithRequest: method intercepts the request on the Objective-C side and checks to see if the requested URL’s scheme matches our custom scheme. If it matches, then pull the args from the URL and do whatever you want with them.

I found several reports of various issues with this method. While playing around I ran into a timing issue where, if I made multiple calls in succession, only the last one would trigger. Your mileage may vary. Check out the source of PhoneGap for a more robust implementation based on this approach that attempts to resolve some of these issues.

Logging from JavaScript

Console logging from JavaScript is a very helpful way to debug scripts and regrettably, it doesn’t automatically work when running in a web view. I tried creating a logging setup based on the method of calling Objective-C from JavaScript described above but kept running into issues if I tried to log multiple entries in a row.

After searching around a bit, I found a comment on a blog post that showed an example based on polling from Objective-C using stringByEvaluatingJavaScriptFromString:.

The idea is to create a queue of log messages in JavaScript and then processing the queue from Objective-C on a periodic basis. This seems to work great.

- (void) processLogger {
  NSString *msg = nil;
  while((msg = [webView stringByEvaluatingJavaScriptFromString:@"logger.dequeue();"]) && msg.length != 0 ) {
    NSLog(@"%@", msg);
  }
  [self performSelector:@selector(processLogger) withObject:nil afterDelay:0.1];
}

The logger object is created in JavaScript as follows:

var Logger = function () {
  self.queue = [];
}
Logger.prototype.log = function (msg) {
  self.queue.push(msg);
}
Logger.prototype.dequeue = function () {
  return self.queue.shift();
}
var logger = new Logger();

Conclusion

Using HTML, CSS, and JavaScript from within a web view is another tool at our disposal when developing iOS apps. Although it is not without challenges, adding functionality to an app with web technologies can be a viable alternative to developing in Objective-C. It is not ideal for all circumstances, but can be beneficial for some tasks.

A sample project that demonstrates these techniques is available on github.

8 thoughts on “Fun with UIWebView

  1. Great Post! So great that I was already planning to write something similar myself in a couple weeks :D . I’m with you all the way on reducing Obj-C to what is most critical and leaving the rest to the HTML realm.

    Cheers!

    1. Thanks Kyle.

      I am finding more and more places where I think the hybrid approach makes sense. Ideally I would like to have most of the functionality implemented in a web view with only the platform specific or performance critical code done native.

      Porting to other platforms is a big item on my research list and using web technologies where possible should help simplify the process.

  2. With a little work you could pass selector names from JS into Obj-C and invoke those selectors. I really like the dynamic message dispatch of Obj-C :)

    1. Thanks for the tip. I’ll play around with it.

      Having a dynamic handler in shouldStartLoadWithRequest: could certainly simplify supporting multiple calls from JavaScript.

  3. This is the “simpler” way of doing it, but the timing problem that you describe is a deal-breaker for many applications that simply can’t afford the risk of losing an invocation of a call from JavaScript to the native application. If you are in this situation, there is a different architecture that you can use to ensure you never lose a method call.

    Here’s a brief explanation: You replace the parts of your JS code where you issue a “js2objc:” URL request with a call to a function which puts the string-serialized form of your invocation onto a queue. Obviously you will want to design a nice API for doing this. In the Objective-C application, you set up an NSTimer to poll the (JS) queue, using stringByEvaluatingJavaScriptFromString:, every half-second or so. If the timer dequeues a serialized invocation, you pass the string form back to the Objective-C code and it can take the appropriate action.

    1. Thanks Erik. This sounds similar to the approach I’m using to log messages from JavaScript, but instead of messages for the console you queue up serialized method calls to process on the Objective-C side. Very nice. I think this is the way to go.

  4. I’m building an open-source library to help make using JavaScript from Objective-C code feel more “natural”. You might find it helpful as well:
    https://github.com/newyankeecodeshop/GAJavaScript

    It implements some of the same techniques you describe, but provides some extension mechanisms to make data marshaling and callbacks easier. It even supports using blocks.

Comments are closed.