If you want to display HTML content from within a native iOS App, UIKit
provides you with a class for just that purpose, UIWebView
. In this post
we will look at setting up and loading a web view, getting messages when
interesting things happen, and then communicating between the App and the
HTML content using JavaScript.
To get the most out of this post download the sample code and follow along with the instructions.
Creating and loading a UIWebView
Edit the main view controller add a property to hold a reference to the web view.
@interface MyViewController : UIViewController
@property UIWebView *webView;
@end
In your view controllers viewDidLoad
method instantiate an instance
and add it to the view heirarchy.
- (void)viewDidLoad
{
[super viewDidLoad];
self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:self.webView];
}
If you run your project now, you'll have an empty web view displayed as
we haven't asked it to load any content. UIWebView
has three different
methods to supply its content, which you use depends your project.
- (void)loadRequest:(NSURLRequest *)request;
- (void)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;
loadHTMLString:baseURL:
and loadData:MIMEType:textEncodingName:baseURL:
can be used if you have already loaded your content and have a string or
data representation.
If you haven't loaded your content loadRequest:
will take care of that
for you. An NSURLRequest
can load content from the network or disk
(with a file://
URL) or any other URL protocol the system is aware of.
Its worth stating here that UIWebView is not limited to loading HTML content, you can also load PDFs, Office Documents, RTF Documents, various image formats, etc. so are a very flexible aproach to getting your content displayed on screen.
Lets add a method to load some content in our web view. We're going to
get the URL for a file in our sample project. To do that we'll ask our
main bundle (the application bundle) for the URL of the content.bundle
(the folder on disk that contains our HTML). Next we'll use get a
relative URL to the index.html
file within that bundle, create a
NSURLRequest
with it and ask the web view to load it. Finally we'll
add a call to that method at the bottom of our viewDidLoad
method.
- (void)viewDidLoad
{
...
[self loadWebView];
}
- (void)loadWebView
{
NSURL *htmlBundleUrl = [[NSBundle mainBundle] URLForResource:@"content" withExtension:@"bundle"];
NSURL *url = [NSURL URLWithString:@"index.html" relativeToURL:htmlBundleUrl];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[self.webView loadRequest:request];
}
In overview, we're getting a file system URL for an HTML file in our application and asking the web view to load it.
Adding a delegate
UIWebView
has a delegate protocol UIWebViewDelegate
to allow you to
receive information about what a web view is doing, and instruct it
which URL requests it should try to load.
UIWebViewDelegate
defines the following optional methods.
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
webViewDidStartLoad:
and webViewDidFinishLoad:
allow your delegate to
perform actions when you start and finsih loading content in your web view.
For example you may want to hide your web view while its loading and animate
showing it when the content has finished loading.
webView:didFailLoadWithError:
is provided to allow you to recover from
a failure to load.
The final, more interesting method, webView:shouldStartLoadWithRequest:navigationType:
allows your delegate make decisions about which requests it should load.
Lets add our view controller as the web views delegate and stub out these methods to log to the console so we can see the order these delegate methods are called when loading our content from disk.
First we need to let the compiler know our view controller conforms to the
UIWebViewDelegate
protocol in our header file.
@interface MyViewController : UIViewController <UIWebViewDelegate>
Now we'll add all the delegate methods, but just have them log to the console.
- (void)webViewDidStartLoad:(UIWebView *)webView
{
NSLog(@"Started loading");
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSLog(@"Finished loading");
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSLog(@"Failed to load with error: %@", error.localizedDescription);
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *url = [request URL];
NSLog(@"Should load url: %@", url);
return YES;
}
Make sure webView:shouldStartLoadWithRequest:navigationType:
returns
YES
or none of your content will load.
Finally we just need to tell our web view that we want to be its delegate,
we'll do this in the view controlers viewDidLoad
method.
- (void)viewDidLoad
{
[super viewDidLoad];
self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
self.webView.delegate = self; // Add this line
[self.view addSubview:self.webView];
[self loadWebView];
}
Build and run your project and you should see the messages output to the console.
Communicating with your web view content
Now you've got a web view loaded with some of your content we can start
to communicate with it. To communicate with the HTML from the App you
can call down directly with JavaScript. UIWebView
has the method
stringByEvaluatingJavaScriptFromString:
that can execute a JavaScript
string in the web views context.
Add the following helper method to your MyViewController
. The method:
- Accepts an
NSDictionary
. - Encodes it as JSON string (using a category included in the sample project).
- Creates a JavaScript string to call
appBridge.messageFromApp
. - Executes the JavaScript in the context of the web view.
If the JSON encode fails the the method will fail silently.
- (void)sendMessageToSite:(NSDictionary *)dict
{
NSString *json = [NSString my_JSONStringWithObject:dict];
if (json) {
// We encoded some json data
NSLog(@"SENDING MESSAGE TO SITE: %@", json);
NSString *javascript = [NSString stringWithFormat:@"appBridge.messageFromApp(%@);", json];
[self.webView stringByEvaluatingJavaScriptFromString:javascript];
}
}
Next lets add a method to handle messages the App receives from the web
view, we'll have the method follow a similar format and accept an
NSDictionary
as its argument.
- (void)receiveMessageFromSite:(NSDictionary *)dict
{
NSLog(@"RECEIVED MESSAGE FROM SITE: %@", dict);
}
For now we'll just log the message to the console.
The content in the web view doesn't have any way to directly call our
Objective C code so we can't use the same approach to send messages back
up. What we can do is use
webView:shouldStartLoadWithRequest:navigationType:
to hijack certain
requests and iterpret them as messages from the web views content.
If you look at the JavaScript code in the main.js
file in the
content.bundle
you'll see a method called AppBridge.prototype.messageToApp
that constructs a URL starting with the scheme js-frame:
and followed
by a JSON blob. It then passes these URLs to
AppBridge.prototype.loadLocationInTemporaryIframe
to be loaded.
With that information in mind we can update our
webView:shouldStartLoadWithRequest:navigationType:
method.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *url = [request URL];
NSLog(@"Should load url: %@", url);
NSString *scheme = [url scheme];
if ([scheme isEqualToString:@"js-frame"] == YES) {
// The site is trying to send us a message.
// Create some data from the url resource specifier
NSString *resourceSpecifier = [[url resourceSpecifier] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
id jsonObj = [resourceSpecifier my_objectFromJSON];
if (jsonObj && [jsonObj isKindOfClass:[NSDictionary class]] == YES) {
// If we didn't error and we got a dictionary call receiveMessageFromSite
[self receiveMessageFromSite:(NSDictionary *)jsonObj];
}
// Don't load a js-frame request
return NO;
}
return YES;
}
In the above code we first get the URL scheme and see if it matches our
messaging scheme js-frame
, if it doesn't then we just return YES
to
allow the URL to be loaded.
After we determine the URL is one we want to process we get its
resourceSpecifier
which is just the rest of the URL after the scheme
and colon. For our js-frame
URLs this should be a JSON blob, so we try
and decode it. If it decodes to an NSDictionary
we pass it to our
receiveMessageFromSite:
helper method. We always return NO
for
js-frame
URL requests as we don't want to load any data for them from
the network.
Update receiveMessageFromSite:
to send a message back to the site
when it receives a message.
- (void)receiveMessageFromSite:(NSDictionary *)dict
{
NSLog(@"RECEIVED MESSAGE FROM SITE: %@", dict);
[self sendMessageToSite:@{
@"message" : @"test"
}];
}
We now have two way communication from our App to the web views content. Give the App a run and you should see a message from the site get output to the console, and the message from the App get added the sites DOM.
Conclusion
We have covered loading a web view with content from our application bundle and one methode of communicating between that content and the App. This approach will work for iOS 5 and above, and if you swap out the JSON serialiation should work on previous versions too.