Chapter    6

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.

9781484209325_Fig06-01.jpg

Figure 6-1. Simple hybrid app screens

This hybrid app performs the following tasks:

  • The content is implemented as an HTML page and rendered in an UIWebView that takes up all the space under the iOS-native UINavigationBar.
  • To change the image from your iOS code, select the UISegmentedControl.
  • To change the image from your JavaScript code, click the <img> element. This also changes the native UISegmentedControl selection.

Follow the usual steps to get started, shown here:

  1. Create an iOS project using the Xcode Single View Application template. Name the project HybridApp. You immediately get a storyboard with one ViewController.swift file that pairs with the view controller scene, as shown in Figure 6-2.

    9781484209325_Fig06-02.jpg

    Figure 6-2. Creating an iOS project using the Single View Application template

  2. Drag a web view from the Object Library onto the root view in the view controller scene.
    1. Position and size it to take up the whole space.
    2. With the web view selected, select Resolve Auto Layout Issues image Reset to Suggested Constraints to automatically add zero-space constraints to its parent view.
  3. With the view controller in the storyboard selected, from the Xcode menu bar, select Editor image Embed In image Navigation Controller. You get a navigation controller scene, a navigation item, and a navigation bar in the view controller scene.
  4. With the navigation item selected, drag a segmented control from the Object Library onto the right side of the navigation bar, as shown in Figure 6-3.

    9781484209325_Fig06-03.jpg

    Figure 6-3. Drawing a navigation bar and a segmented control in the right bar button

  5. Use the Assistant Editor to connect the following storyboard outlets to your code in ViewController.swift. Listing 6-1 depicts the storyboard operations results.
    1. Connect a web view delegate to the ViewController class.
    2. Connect a web view referencing outlet to the ViewController mWebview stored property.
    3. Connect a segmented control referencing outlet to the ViewController mSegmentControl stored property.
    4. Connect a segmented control value changed event to the ViewController onValueChanged(...) IBAction function.
    5. Declare a ViewController that implements UIWebViewDelegate protocol. (You will implement the protocol methods later.)
    6. To make sure the previous storyboard tasks are working, you can add a line in viewDidLoad and some logging code just to see whether all the outlets are good.

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.

9781484209325_Fig06-04.jpg

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:

  1. Develop your web content as usual. Instead of deploying the web content to the web server, bundle it in the HybridApp project.
    1. Figure 6-5 shows a simple web content root folder. You can organize your web content with subfolders like you normally do.

      9781484209325_Fig06-05.jpg

      Figure 6-5. HybridApp web content

    2. Listing 6-2 depicts the demo index.html file that contains two inline JavaScript functions. You will use them in later sections.

      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>
  2. Drag the WebContent root folder from the iOS Finder to the Xcode HybridApp project.
    1. This is similar to adding image assets to your Xcode project, but make sure you select Create Folder Reference to preserve the URL path, as shown in Figure 6-6.

      9781484209325_Fig06-06.jpg

      Figure 6-6. Adding web content to the Xcode project

    2. You may directly modify index.html or any web content in the Xcode editor from now on. For example, you can remove the <h1> element in the index.html file using the Xcode editor. Figure 6-7 depicts the results with the WebContent folder in the HybridApp project.

      9781484209325_Fig06-07.jpg

      Figure 6-7. WebContent folder in Xcode

  3. Listing 6-3 depicts how to render the local web content in UIWebView.
    1. Obtain the file path for the bundled resource using NSBundle.pathForResource(...).
    2. You still use the same UIWebView.loadRequest(...) method to load the file URL.

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:

  1. You can inject any JavaScript code, including declaring the whole function. Previously in Listing 6-2, the inline replaceImg(src) JavaScript function replaces the src attribute in the <img> DOM element, which you can easily invoke from your native Swift code (see Listing 6-4).

    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;
       }
       ...
  2. Listing 6-5 changes the image in UIWebView by invoking the replaceImg(...) JavaScript code. This is achieved by injecting JavaScript into UIWebView using stringByEvaluatingJavaScriptFromString.

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:

  1. Previously in Listing 6-2, the <img> onClick() event invokes the onClicked() JavaScript function. In the JavaScript code, build a URL request with a custom URI scheme that only your native code understands.
    1. I chose uiwebview as my custom URI scheme name. You can choose any scheme name as long as it is unique and only your native code understands it.
    2. To pass extra data from JavaScript, I chose to use the URI query part.
    3. To dispatch the request to the appropriate native method, I chose to use the URI hierarchical part.
    4. document.location.href will start loading the URL, which gives UIWebView a chance to intercept the request via callback. See Listing 6-6.

      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.

  2. Implement the UIWebViewDelegate protocol to intercept the URL request, as shown in Listing 6-7.
    1. Intercept the request by the custom scheme name and return false to stop the page load. You just need to know an action is triggered from JavaScript, but you return true without breaking the real page load request.
    2. Since there is only one interface method, I didn’t care that the URI hierarchical part is passed from JavaScript function. If I need to interface with different methods from different JavaScript code, I would use it as the switch-case condition to dispatch calls to appropriate Swift method.
    3. The Swift code updated the segmented control selection and updated the images by calling the existing UISegmentedControl.onValueChanged(...) method discussed previously in Listing 6-5.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset