Several months ago, I wrote a template/tutorial on developing a Native iOS Web App that seized the phone’s camera functionality. Since then it has been a relatively popular article on my blog, iOS5 was released and I’ve also received some questions/feedback. So, I thought it might be appropriate to revise this template, based on the previous one and go over the changes. Download the code here.

Let me begin by saying that this template isn’t anything like or close to Phone Gap. If you are serious about portability and want to remain Objective-C illiterate, Phone Gap is a better alternative. This is just a basic template for those who want a Native iOS Web App with the possibility to dive into as much or as little Objective-C as you like. It’s more like an alternative to Interface Builder. The template comes with camera functionality and some basic Javascript-to-Objective-C message sending.

So lets briefly discuss what we are going to do:

  • This template will use ARC (Automatic Reference Counting) introduced in iOS5
  • We won’t use Interface Builder at all in this template
  • We will instead create a subclass of UIViewController and place a webview on top programatically
  • The class will automatically detect objc:// schema links and try to run that method in a custom delegate class we will build

The HTML stuff

First let’s briefly revise the basic HTML view we will load. You should notice that 3 out of 4 of the links are seemingly dead – these will be linked with Objective-C once inside of the iPhone. I tried to make this template look a little bit nicer looking and iPhone-like than the last one. The buttons are inspired by Chad Mazzola’s CSS3 Buttons and I’m using the jQuery library. If you are loading pages via the web and interested in a smaller Javascript library, check out Zepto

jQuery Alert


Objective-C Alert
Take a camera image
Image from library


index.html

<!doctype html>
<head>
    <meta charset="utf-8">
      <meta name="viewport" content="width=device-width,initial-scale=1">

      <link rel="stylesheet" href="style.css">
      <script src="jquery-1.6.2.min.js"></script>
      <script defer src="script.js"></script>
</head>
<body id="body">
  <div id="container">
      
    <div id="iphone">
        <a href="/" class="js">jQuery Alert</a><br />
        <label for="username">Enter your name:</label>
        <input type="text" id="username" />
        <a href="javascript:objcMessage();">Objective-C Alert</a><br />
        <a href="objc://takeCameraImage">Take a camera image</a><br />
        <a href="objc://takeLibraryImage">Image from library</a><br />
    </div>
      
    <img id="testImage" src="iphonebattery.jpeg" />

  </div><!-- #container -->
</body>
</html>

styles.css

body{
    margin: 0;
    padding: 0;
}
#container {
    padding: 10px;
    font-family: "helvetica neue", helvetica, arial, sans-serif;
    background-image: -webkit-gradient(
	linear,
	left bottom,
	left top,
	color-stop(0.14, rgb(138,138,138)),
	color-stop(1, rgb(199,191,199))
    );
    
}

#iphone a{
    
    display: block;
    width: 300px;
    -webkit-touch-callout: none;
   /* -webkit-user-select: none; */
    background-color: #8C9CBF;
    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #8C9CBF), color-stop(50%, #546A9E), color-stop(50%, #36518F), color-stop(100%, #3D5691));
    background-image: -webkit-linear-gradient(top, #8C9CBF 0%, #546A9E 50%, #36518F 50%, #3D5691 100%);
    border: 1px solid #172D6E;
    border-bottom: 1px solid #0E1D45;
    -webkit-border-radius: 5px;
    -webkit-box-shadow: inset 0 1px 0 0 #b1b9cb;
    color: white;
    font: bold 16px "helvetica neue", helvetica, arial, sans-serif;
    padding: 7px 0 8px 0;
    margin: 0 auto;
    text-decoration: none;
    text-align: center;
    text-shadow: 0 -1px 1px #000F4D;
}
label{
    width: 300px;
    text-align: center;
    padding: 10px;
    font-size: 18px;
}
input#username{
    width: 300px;
    border-radius: 5px;
    text-align: center;
    padding: 5px 0;
    font-size: 24px;
    margin: 0 0 10px 0;
}

#testImage{
    border: 1px solid #CCC;
    width: 100%;
    height: 66%;
}

scripts.js

$(document).ready(function(){ 
    $("a.js").click(
        function(e){ 
            e.preventDefault();
            alert("You clicked on a link that activates javascript"); 
        }
    );          
});

function objcMessage()
{
    var name = "empty";
    if( $("#username").val() != "" )
        name =  $("#username").val();
    window.location = "objc://message/" + name;
}

function processImage( img )
{
    $('#testImage').remove();
    $('#body').append( '' );
}

The XCode Project

Now we can move on to our XCode project. You can build your own or follow along in the sample code.
I’ve used a simple View Based Application. First thing, we will temporarily ignore the UIViewController that XCode makes for us with the template and create our own file from scratch – Call it “WebViewController” and its contents will be as follows:

WebViewController.h

#import <UIKit/UIKit.h>

@class WebViewControllerDelegate;
@interface WebViewController : UIViewController <UIWebViewDelegate>

@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) WebViewControllerDelegate *functionDelegate;

-(void)loadPageWithURL:(NSString *)url;
-(void)loadPageFromFile:(NSString *)html;

@end

WebViewController.m

#import "WebViewController.h"
#import "WebViewControllerDelegate.h"

@implementation WebViewController

@synthesize webView;
@synthesize functionDelegate;

- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //make a frame fore the webview based on the view's frame
    CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
    self.webView = [[UIWebView alloc] initWithFrame:frame];
    [self.view addSubview:webView];
    
    //UIWebViewDelegate will be self
    [webView setDelegate:self];
    // Web Requests that start with the scheme "objc://" will be caught and sent to the WebViewControllerDelegate
    self.functionDelegate = [[WebViewControllerDelegate alloc] init];
    functionDelegate.webViewController = self;
}

- (void)viewDidUnload {
    [super viewDidUnload];
    self.webView = nil;
    self.functionDelegate = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - StackingWebViewController

/* Loads a URL string into the webview */
- (void)loadPageWithURL:(NSString *)url {
    NSURL *theURL = [NSURL URLWithString:url];
    NSURLRequest *theRequest = [NSURLRequest requestWithURL:theURL];
    [webView loadRequest:theRequest];
}
/* Loads page from file */
- (void)loadPageFromFile:(NSString *)html {
    //First we load up the index.html file
    NSString *path = [[NSBundle mainBundle] pathForResource:[html stringByDeletingPathExtension] ofType:@"html"];
    NSData *htmlData = [NSData dataWithContentsOfFile:path];
    
    // Next we need to set up a proper base URL for our files
    NSString *resourceURL = [[NSBundle mainBundle] resourcePath];
    // The URL in the raw still needs some cleaning
    // Need to be double-slashes to work correctly with UIWebView, so change all "/" to "//"
    resourceURL = [resourceURL stringByReplacingOccurrencesOfString:@"/" withString:@"//"];
    // Also need to replace all spaces with "%20"
    resourceURL = [resourceURL stringByReplacingOccurrencesOfString:@" " withString:@"%20"];
    //And make a proper URL
    NSURL *baseURL = [NSURL URLWithString:[NSString stringWithFormat:@"file:/%@//", resourceURL]];
    
    //Finally let's load up the html data and passthe Base URL for the CSS and Javascript files
    [webView loadData:htmlData MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:baseURL];
}

#pragma mark - UIWebViewDelegate

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

    //This will catch clicked links and location changes made from Javascript, but no other request types
    if (navigationType == UIWebViewNavigationTypeLinkClicked || navigationType == UIWebViewNavigationTypeOther)
    {
        NSURL *URL = [request URL]; //Get the URL
        //The [URL scheme] is the "http" or "ftp" portion, for example
        //so let's make one up that isn't used at all -> "objc"
        //
        if ( [[URL scheme] isEqualToString:@"objc"] ) {
            //The [URL host] is the next part of the link
            //so we can use that like a selector
            
            NSString *selectorName = [URL host];
            id data = nil;
            
            NSMutableArray *parameters = [NSMutableArray array];
            if ( ![[URL path] isEqualToString:@""] )
            {
                selectorName =  [NSString stringWithFormat:@"%@:", selectorName];
                parameters = [NSMutableArray arrayWithArray: [[URL path] componentsSeparatedByString:@"/"] ];
                [parameters removeObjectAtIndex:0]; //first object is just a slash "/"
                if ( [parameters count] == 1 ){
                    data = [parameters objectAtIndex:0];
                }
                else{
                    data = parameters;
                }
            }
            
            SEL method = NSSelectorFromString( selectorName );
            if ([functionDelegate respondsToSelector:method])
            {
                //This line may give a warning but that's ok, we are being memory concious
                // See: http://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown
                [functionDelegate performSelector:method withObject:data];
            }
            return NO;
        }
        
    }
    return YES;
}

@end

The code is well commented but let’s look at the functionality of this class from a high level.

  • On viewDidLoad the class instantiates a UIWebView programatically and adds it to the view
  • We set the class as the UIWebView Delegate, so it will listen directly to the UIWebView we added
  • The class has two functions to load either a URL or a local .html file
  • We receive a callback from the UIWebView to ask permission to follow any link with webView:shouldStartLoadWithRequest:navigationType:
  • We listen in the callback specifically for “objc” schemed links and we fire the appropriate method on our WebViewControllerDelegate if it can respond to this method
  • “objc” schemed links can also send string parameters. For example, “objc://doThis/then/that” would run a method

    -(void)doThis:(NSArray *)data

    and the NSArray would have 2 NSStrings : @”then”,@”that”
  • For convenience sake if there is only 1 parameter, it will pass as a
    (NSString *) instead

Speaking of our WebViewControllerDelegate, let’s look at it next:

WebViewControllerDelegate.h

/**
* WebViewController is responsible for all
* messages that can be used in HTML
*/

@class WebViewController;
@interface WebViewControllerDelegate : NSObject <UINavigationControllerDelegate, UIImagePickerControllerDelegate>

@property (nonatomic, weak) WebViewController *webViewController;

/*
* Test alert from Objective-C
 */
-(void)message:(NSString *)name;

/* On successful picture selection, a base64 image
* is sent to the JS function processImage();
*/
-(void)takeCameraImage;
-(void)takeLibraryImage;

@end

WebViewControllerDelegate.m

#import "WebViewControllerDelegate.h"
#import "NSData+Base64.h"
#import "WebViewController.h"

@implementation WebViewControllerDelegate

@synthesize webViewController;

-(void)message:(NSString *)name
{
    //Showing a basic pop up alert
    
    NSString *message = [NSString stringWithFormat:
                         @"Your name is %@", name ];
    
    UIAlertView *alert = [[UIAlertView alloc] 
                          initWithTitle:@"Message From OBJ-C" 
                          message:message 
                          delegate:nil 
                          cancelButtonTitle:@"OK" 
                          otherButtonTitles:nil, nil];
    
    [alert show];
}

-(void)takeCameraImage
{
    if ( ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] )
    {
        NSString *message = [NSString stringWithString:
                             @"Your device does not have a camera" ];

        UIAlertView *alert = [[UIAlertView alloc]
                              initWithTitle:@"No camera available"
                              message:message
                              delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil, nil];

        [alert show];
        return;
    }

    // Set the UIImagePicker, set it to theCamer and set self as the delegate 
    UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
	imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
	imagePicker.delegate = self;
    
    // Present the image picker
	[webViewController presentModalViewController:imagePicker animated:YES];

}

-(void)takeLibraryImage
{
    // Set the UIImagePicker, set it to theCamer and set self as the delegate
    UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
	imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
	imagePicker.delegate = self;

    // Present the image picker
	[webViewController presentModalViewController:imagePicker animated:YES];

}

#pragma mark - Image Picker

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    //Get the Image
	UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
    
    //flatten it to NSData as a JPEG, low quality
    NSData *flatImage = UIImageJPEGRepresentation(image, 0.1f);
    
    // convert NSData to a base 64 encoded string
    // NSData+Base64 Category provided by Matt Gallagher
    // http://cocoawithlove.com/2009/06/base64-encoding-options-on-mac-and.html
    //
    NSString *image64 = [flatImage base64EncodedString];
    
    //process the image in javascript to be added to the page
    NSString *js = [NSString stringWithFormat: @"processImage('data:image/jpeg;base64,%@')", image64];
    [webViewController.webView stringByEvaluatingJavaScriptFromString:js];
    
    //dismiss the image picker
    [picker dismissModalViewControllerAnimated:YES];
	
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
    
    //cancel was hit inside of the camera view
    [picker dismissModalViewControllerAnimated:YES];
}
@end

The WebViewControllerDelegate is only responsible for dealing with methods that are intended to be called from HTML. In my previous template, the equivalent delegate class was also the UIWebViewDelegate, so it was the gatekeeper for following links as well. However, I feel it is wrong to do this for one important reason: You should have a fresh delegate capable of only responding to methods you actually want exposed to HTML. Using the old method from my previous template, we were effectively exposing methods more methods that we wouldn’t really ever want to call. With that, you could argue that my UIImagePickerController related methods shouldn’t even be in this delegate. If this template was more complex, I would have moved it out.

To summarize the functionality we are seeing:

  • The message: function can accept a single string parameter and tell you your name. We set up a corresponding method in javascript that took the name from the input box and added it as a parameter. (As an aside, you could use a “/” in your name and see how multiple parameters work without crashing the app)
  • We have a takeCameraImage: function which checks if your device has a camera and then launches a UIImagePickerController
  • We also have a takeLibraryImage: function which opens the photo library instead
  • After selecting an image, a Javascript function processImage( img ) is called to pass over the Base64Encoded image from Obj-C

One last step

So with that, we are set up and ready to do some Native iOS HTML’in , there’s just one more thing. Go back to our original UIViewController that we ignored at the start and subclass it from WebViewController instead, like so:

ViewController.h

#import <UIKit/UIKit.h>
#import "WebViewController.h"

@interface ViewController : WebViewController

@end

ViewController.m

#import "ViewController.h"

@implementation ViewController

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - View lifecycle
/* We are ready to load a page using our new webview functionality */
- (void)viewDidLoad
{
    [super viewDidLoad];

    [self loadPageFromFile:@"index.html"];
    //[self loadPageWithURL:@"http://www.google.ca"];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
    } else {
        return YES;
    }
}

@end

And it’s just that easy. In one simple line in our viewDidLoad we are now capable of loading HTML files that can call exposed obj-c methods that we write. The nice thing about this template, is you can subclass it to your heart’s content and reuse the functionality easily in multiple places across a more complex app.

In Summary

So now you have your Native iOS5 ready Web App. You can now code your views exclusively in HTML/CSS/Javascript and take full advantage of an Objective-C backend on your iPhone as desired. If you have any questions, let me know and I will try and respond/amend asap.

Download the code here.

Post filed under Blog and tagged , .

  • Aldrin Pereira

    Hi Kyle, this is good stuff.

    Could you advise what would be the best way to load a page using a link on the first page. For example if there was a link on index.html that causes the program to interact with some web services following which it needs to land on a submitted.html page. How would this be done? Thanks. Aldrin

  • Anonymous

    Hi Aldrin,

    I’m not sure I fully understand what you are trying to accomplish but a couple of ideas:

    1. Javascript could call a function that loads a different .html page
    2. If objective-C is already doing something like processing information passed through, it can load another page itself and you can always inject new javascript into that page with the “stringByEvaluatingJavaScriptFromString:” method available to the UIWebView

    I hope that’s useful

  • Aldrin Pereira

    Hi Kyle

    Let’s say if I wanted the initial page to have a user name, password and submit button to post to an external page on the Internet, but prior to posting the form, I needed a check to determine if the external website was accessible at the moment. If the website was unavailable, some friendly message or a ‘Please try later’ page is intended to be shown.

    The second point of your advise to me is what I needed to be certain was possible. As I am very new to IOS development (just two weeks of practice), I was unsure what was required to load another page (the available external or the ‘Try later’ internal page) after processing the connectivity bit.

    On the other hand, your first point on Javascript is very easy for me to implement. I know that the availability check, the form post and subsequent redirects can very easily be managed by jQuery.

    Thanks a lot for the advise, it is much appreciated for this has provided me with a working template that can used in basic web related apps and at a later stage for more complex ones.

    A suggestion for making your already great template more perfect would to add the below line (marked with a comment) to the viewDidLoad method

    CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
    self.webView = [[UIWebView alloc] initWithFrame:frame];
    self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth; // Add this line
    [self.view addSubview:webView];

    This addition seemed to fix an issue noticed while testing the build in the iPhone landscape mode of the simulator whereby the canvas did not use up the entire screen.

    Thanks once again.

  • Aldrin Pereira

    I apologise for posting my reply as a new comment.

    Regards
    Aldrin