Bonus Chapter: Hybrid Apps
For serious web developers using JavaScript to create interactive web apps, UIWebView provides a seamless way to transfer those skills to creating mobile apps. UIWebView also allows native code to interface with the JavaScript that is loaded with the web page. Both iOS and Android inject JavaScript code from the native code into the embedded browser. Web developers normally think it is easier to use their familiar development skills to create their web apps, but using this hybrid approach opens up a different way to create mobile apps.
The so-called hybrid approach has gained a bit of attraction in recent years; there are many cross-platform compilers aimed at JavaScript developers who want to create mobile apps. To name a few, PhoneGap, Apache Cordova, Titanium, Icenium, Trigger.IO, and so forth, all seem to be doing a good job for the intended purpose.
The key is how to interface the code between JavaScript and native iOS SDK to make it bi-directional. It is relatively easier to have your native iOS code interface with your JavaScript code because this process is well-documented using the existing iOS API. However, how to call iOS code from your JavaScript code doesn’t seem to be well-known yet, so I will reveal the secret that’s used in those third-party frameworks. The process is actually very easy.
To learn by example, you will be implementing an iOS app that calls functions between your Swift code and your JavaScript code. Figure 6-1 shows the completed iOS app you will be creating.
Figure 6-1. Simple hybrid app screens
This hybrid app performs the following tasks:
Follow the usual steps to get started, shown here:
Figure 6-2. Creating an iOS project using the Single View Application template
Figure 6-3. Drawing a navigation bar and a segmented control in the right bar button
Listing 6-1. Storyboard Outlets in ViewController.swift
import UIKit
class ViewController: UIViewController, UIWebViewDelegate {
@IBOutlet weak var mWebview: UIWebView!
@IBOutlet weak var mSegmentControl: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
mWebview.loadHTMLString("<h1>Hello HybridApp</h1>", baseURL: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func onValueChanged(sender: AnyObject) {
println(">> onValueChanged")
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
return true
}
}
Nothing you’ve done here is new yet. It was the same storyboard tasks as usual. Run and test the HybridApp project. You should see the HybridApp app as shown in Figure 6-4.
Figure 6-4. HybridApp storyboard tasks
Bundle Web Contents
Recall that in the UIWebView code in Chapter 4’s Listing 4-18, you used UIWebView.loadRequest(NSURLRequest) to load the remote URL. You can bundle the web contents with the iOS app and still use the same API to load the web contents using a file URL to the bundled local files. Do the following:
Figure 6-5. HybridApp web content
Listing 6-2. The Simple Web content-index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
function onClicked() {
console.log('onClicked'),
var name = document.getElementById('imgId').name;
// <scheme name> : <hierarchical part> [ ? <query> ] [ # <fragment> ]
document.location.href = 'uiwebview://onClicked?img=' + name;
}
function replaceImg(src) { // to be called from Swift code
console.log('replaceImg: ' + src);
document.getElementById('imgId').src = src;
document.getElementById('imgId').name = src;
}
</script>
<title>Hybrid App</title>
</head>
<body>
<div>
<H1>My Image</H1>
<img id=imgId src='img0.png' name= 'img0.png' onClick="onClicked();" />
</div>
</body>
</html>
Figure 6-6. Adding web content to the Xcode project
Figure 6-7. WebContent folder in Xcode
Listing 6-3. Load Bundled Web Contents
class ViewController: UIViewController, UIWebViewDelegate {
@IBOutlet weak var mWebview: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var htmlfile = NSBundle.mainBundle().pathForResource("index", ofType: "html", inDirectory: "WebContent")
var htmlfileUrl = NSURL(fileURLWithPath: htmlfile!)
// mWebview.loadHTMLString("<h1>Hello HybridApp</h1>", baseURL: nil)
mWebview.loadRequest(NSURLRequest(URL: htmlfileUrl!))
}
You can build and run the iOS HybridApp project to see the bundled web contents being rendered in offline mode.
Invoke JavaScript Function
To call JavaScript functions from iOS code, you simply inject JavaScript code into UIWebView. The following steps walk you through a typical usage that invokes a JavaScript function from iOS code:
Listing 6-4. The replaceImg(...) JavaScript Function Inline in index.html
...
<script>
...
function replaceImg(src) {
console.log('replaceImg: ' + src);
document.getElementById('imgId').src = src;
document.getElementById('imgId').name = src;
}
...
Listing 6-5. Inject JavaScript Code into the Embedded Browser, UIWebView
class ViewController: UIViewController, UIWebViewDelegate {
...
@IBAction func onValueChanged(sender: AnyObject) {
var selection = (sender as UISegmentedControl).selectedSegmentIndex
var img = selection == 0 ? "img0.png" : "img1.png"
var jsCode = "replaceImg('" + img + "')" // js: replaceImg('img2.png')
self.mWebview.stringByEvaluatingJavaScriptFromString(jsCode)
}
...
You can build and run the iOS HybridApp project and select the UISegmentedControl to change the image in the UIWebView.
Invoke Native Code
UIWebView goes through the life cycle defined in the UIWebViewDelegate protocol when loading a URL. The following approach intercepts the UIWebView’s early life-cycle events and takes the chance to call your Swift code:
Listing 6-6. The Simple Web content-index.html
<html>
...
<script>
function onClicked() {
console.log('onClicked'),
var name = document.getElementById('imgId').name;
// <scheme name> : <hierarchical part> [ ? <query> ] [ # <fragment> ]
document.location.href = 'uiwebview://onClicked?img=' + name;
}
...
Note URI is defined as consisting of four parts: <scheme name> : <hierarchical part> [ ? <query> ] [ # <fragment> ]. As long as you set them, you will be able to intercept them easily in your native code.
Listing 6-7. Intercept the Custom Page Load Request
class ViewController: UIViewController, UIWebViewDelegate {
...
let CUSTOM_SCHEME = "uiwebview"
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
println(">>shouldStartLoadWithRequest")
var url = request.URL
if let scheme = url.scheme {
if scheme.lowercaseString == CUSTOM_SCHEME {
var host = url.host // URI hierarchical part, not used
var queryString = url.query!
println("queryString: (queryString)");
var img = queryString.componentsSeparatedByString("=")[1]
var isImg0 = img == "img0.png"
self.mSegmentControl.selectedSegmentIndex = isImg0 ? 1 : 0
self.onValueChanged(self.mSegmentControl)
return false
}
}
return true
}
Build and run HybridApp. You can select the UISegmentedControl or click the <img> element in the UIWebView to change the image in the UIWebView.
This completes the bi-directional interface between JavaScript and iOS native code. It is much lighter than those popular cross-platform frameworks and has great extensibility. In most cases, you really don’t need those heavy cross-platform frameworks that often offer too many features for your apps.