iOS

A Look at the WebKit Framework in iOS 8 – Part 2


In the first part of the WebKit tutorial, we covered the basics of the WebKit framework. In this tutorial, we’ll dive deeper into WebKit and look at how we can customize web pages loaded by our native app. We’ll also see how we can extract data from web pages and use it in our app.

We’ll build an app that is specifically meant to browse appcoda.com. To follow along, download the starter project. The starter project is a simple browser named Coda, much like the one we built in the first part of this tutorial. The only difference is that it doesn’t have a text field for the user to input a url to be loaded and I replaced the Back, Forward and Reload buttons text with icons.

webkit-javascript

Handling External Links

If you run the app and click on an external link, the web view will load that link’s content. The app is meant to be a dedicated Appcoda browser therefore we’ll have to prevent the loading of external links. If a user taps an external link, the page will be opened in Safari.

What we want here is to customize the way pages are loaded. To do this, we have to intervene with the usual page loading process. Before we look at how this is done, let’s look at the page loading process.

The page loading process starts with an Action. This is anything that triggers a page load like tapping a link, using the back, forward or reload buttons, JavaScript setting the window.location property, subframe loading or a call to WKWebView loadRequest(). Then a Request is sent off to the server and we get back a Response (this could be a positive response or an error message like 404). Then the server sends back some Data and the process completes.

webkit demo project

WebKit allows your app to inject itself after the Action and Response phases, and to decide whether to continue the load, cancel it or tweak it according to your needs.

webkit demo project

Add the following method to ViewController.

The above is a WKNavigationDelegate protocol method that gets called several times during the page load. One of its parameters is an WKNavigationAction object which contains information that can help you decide whether to continue page load or not. In the above code, we use two of its properties, navigationType and request. We only want to interrupt external links that have been initiated by a user and so we check for the navigationType. We then check the request url to determine whether it is an external link. If both conditions are met, then the URL is opened by the device’s browser (Safari) and WKNavigationActionPolicy.Cancel stops the process. Otherwise the page loads as usual and renders in the web view.

Run the app and any tap on an external link will open up Safari which will load the page.

Setting the Page Title

It would be useful to have the page title showing as an indicator to where the user is on the website. In the previous article we looked at some observable properties of WKWebView like loading and estimatedProgress. title is another observable property which we’ll use to get the title of the currently loaded page.

Add the following to viewDidLoad() right underneath the other calls to addObserver()

Then in observeValueForKeyPath(_:, ofObject:) add the following at the bottom of the function, right after the other if statements.

Run the app and browse around and the title on the navigation bar will update accordingly.

image03

Modifying Web Page Content

The Coda app now works well as an exclusive Appcoda browser, but there are a few things we can do to improve the user experience.

By their very nature, mobile apps are known to present data and information in a concise way. Users expect to see just the information they need and not have to scroll through a lot of other data to get to it.

At the moment, the Coda app displays everything on the Appcoda web page. We want to omit some things that aren’t related to the content. We’ll remove the sidebar and the section at the bottom of the page that shows the Appcoda Swift book.

To do this, we’ll use JavaScript to inject CSS rules into the web page that will hide these sections. First we need to inspect the website and determine the elements to target.

To inspect a webpage, you use the Developer tools which are usually available in all the major browsers. You can also install them as plugins/add-ons to your browser e.g. Firebug for Firefox. I am going to be using the Chrome Developer tools but you can use whichever browser and tools you want. The process will basically be the same.

To open the Chrome developer tools, go to View > Developer > Developer Tools.

This will open the Developer window at the bottom of the screen. The Developer window is split into sections with the top showing the page source on the left and CSS on the right. At the bottom, is the JavaScript Console where you can write your code and have it executed on the page.

We need to check for the id attributes that mark the sections we want to hide.

The sidebar is found on all pages on the site but the book section is only found on an article page. Click on any article and open the Developer Tools. To start off, right click on the sidebar and select Inspect Element. In the Developer tools window, the html code corresponding to the place you clicked on will be highlighted. If you hover your mouse on the code, a highlight will appear on the area in the webpage that corresponds to the code. We want to get an id (or class) of the root element that encloses the whole sidebar.

Depending on where you landed on when you selected Inspect Element, move upwards collapsing the tags and checking to see that it is only the sidebar that is highlighted on the page. The last tag to be collapsed will be the root element. In our case, it is a div tag with an id of ‘sidebar’.

It is best to first write and test your code in the browser before taking it into your app, because if you get it wrong, debugging it there would be a lot harder. We’ll test our CSS and JavaScript on the browser first.

Click on the div tag that we’ve identified above. On the right of the view, you’ll see its CSS styles. Click on the + button in that window which will add a style rule as shown below.

For the above rule add the following:

When you add the above, the sidebar should disappear from the page.

Delete the style rule to bring the sidebar back. We’ll now add the code to the DOM with JavaScript. Below the html view, is the JavaScript console. Paste the following to the console.

The above creates the element and assigns it to a variable. Next add the following which will add the css rule to the style element we created. I have included the class name for the book section as well.

Lastly, add the following to add the style tag to the DOM. Immediately the code is executed, the sidebar and book section will disappear from the page.

The above process is what is required to hide sections from a web page.

Back in Xcode, create a new file with File > New > File > iOS > Other > Empty and name it hideSections.js. Add the following to the file.

In ViewController, replace init() with the following.

The above code creates a WKWebViewConfiguration object which holds some properties that allow the creation of the bridge between native code and the hosted web content. The JavaScript is then loaded and wrapped in an instance of WKUserScript. The script is then added to the configuration’s userContentController and then the webView is initialized with the configuration.

When creating the instance of WKUserScript, we specify when the script should be injected and whether it acts on the whole page or a specific frame.

Run the application and you’ll no longer see the sidebar(in iPhone, this was appearing towards the bottom of the page) and book section content.

Extracting Data From the Web Page

The homepage of the Appcoda website shows a summary of the last 10 articles to be posted. When viewing this page on our web view, you have to do a lot of scrolling to get to the bottom articles. We want to have an easier way to get to the recent articles. We’ll create a table view which will hold a list of the recent articles.

We’ll get this list by extracting data from the homepage. I won’t go into inspecting html here again. I’ll just give the JavaScript I used to extract the posts and explain what it does.

If you run the following in the JavaScript console while you are on the homepage, a list of post titles and their urls will be printed to the console.

If you look at the structure of html for the posts on the page, it will be something as shown below.

In the JavaScript code above, we get the element with the id of ‘content’. This is the div that is the immediate parent of the list of posts. We then get all elements which are under this div and assign them to the variable posts. This will hold an array of the post divs. We then iterate over this array, and get the text content of the anchor tag that is found in the h2 element of each post div. We also get the value of the anchor tag’s href attribute which holds the URL of the post. We then print this to the console.

In Xcode, create a new file with File > New > File > iOS > Other > Empty. Name it getPosts.js. Paste the following to the file.

The above gets the values of all the post titles and urls and saves them to an array. The last line enables JavaScript and native code communication. webkit.messageHandlers is a global object that facilitates the triggering of callbacks in the native code. didGetPosts represents a message with the same name in the native code. postMessage passes data through to the callback.

In the storyboard, drag a Bar Button Item to the navigation bar and place it on the left side. Change its text to ‘Recent’. Create an outlet for it and name it recentPostsButton. You should have the following in code.

At the bottom of viewDidLoad(), add the following. We want the button disabled until the posts list is loaded.

In ViewController, add the following below the import statements.

Add the following property to the class.

Add the following to the bottom of viewDidLoad()

Here we load the JavaScript just like we did with the other JavaScript file, only this time we want to inject it .AtDocumentEnd when the whole DOM has been constructed. We also add the MessageHandler to the
WKWebViewConfiguration and initialize an instance of WKWebView with the configuration before loading a request with the URL of the homepage.

Update the class declaration so that it conforms to the WKScriptMessageHandler protocol.

We’ll create a model class to hold the post data. Create a class with File > New > File > iOS > Source > Cocoa Touch Class. Name it Post and make it a subclass of NSObject. Paste the following to the class.

Add the following variable to ViewController.

Add the following required WKScriptMessageHandler protocol method to the class.

This checks to see if the name of the received message matches with what we are expecting and if it does, it extracts the array of dictionaries attached to the message and creates Post objects with this data before adding each post to the posts array. Then the recentPostsButton is enabled.

Open the storyboard and add a Table View Controller onto the canvas. Select it and embed it in a navigation controller with Editor > Embed In > Navigation Controller.

Control-drag from the Recent button on the View Controller scene to the new navigation controller and select popover presentation from the popup. Select the segue that has been added and set its Identifier to recentPosts.

Create a new file with File > New > File > iOS > Source > Cocoa Touch class. Name it PostsTableViewController and make it a subclass of UITableViewController.

In the storyboard, select the Table View Controller and set its class to PostsTableViewController in the Identity Inspector. Select the prototype cell of the table view and set its Identifier in the Attributes Inspector to postCell.

Edit the PostsTableViewController class as shown.

Here we implement the table view controller data source methods that will populate the table view with the post titles.

Add the following to ViewController.

This is called when the Recent button is tapped, right before the table view controller is shown. It passes the list of posts to the table view controller.

Run the app. On tapping the Recent button, you will be presented with a table view with a list of recent articles. On the iPhone, this takes up the whole screen but on the iPad, it appears in a popover.

image04

When you tap on a table cell, nothing happens. We want the tapped on article to be loaded on the web view.

In ViewController, add the following below the import statements.

We will be posting a notification when a table cell is selected. The above constant holds the name of the notification.

In PostsTableViewController add the following method.

This posts a notification each time a cell is selected and dismisses the table view controller.

In ViewController, add the following at the bottom of viewDidLoad()

The above sets the view controller as an observer of the notifications posted when a cell is selected in the table view.

Add the following to ViewController.

This gets the post object attached to the notification and loads the post URL in the web view.

Run the application and you should be able to navigate to any recent article you select from the table view.

At the moment, when we tap on the Recent button, we have no way of dismissing the table view, unless we select a recent article to be loaded. We’ll add a cancel button.

In the storyboard, drag a Bar Button Item to the right hand side of the navigation bar in the table view controller. Set its Identifier to Cancel in the Attributes Inspector.

Open Assistant Editor and control-drag from the Cancel button to the PostsTableViewController class to create an action. Name it cancel and make sure the Type is set to UIBarButtonItem. Edit the action as shown.

You should now have a Cancel button that dismisses the table view when tapped.

image05

Conclusion

The new WebKit framework gives developers the ability to create apps that interact seamlessly with hosted web pages. We’ve looked at how to customize a web page’s appearance, extract data from it and use the data in our app’s interface.

If your app is a container for web content, then using the WebKit framework will result in an app whose performance and look-and-feel are on par with native apps. WebKit might be a game-changer for such apps, which previously felt sluggish to use.

If you want to find out more about the framework, this WWDC video is a good place to start.

You can download the completed project here.

iOS
How to Beta Test Your App Using TestFlight
iOS
Using Diffable Data Source with Collection Views
Tutorial
Parse Migration Part 3: Setting up Cloud Code, Dashboard, and Push Notifications on Parse Server
  • Chris Kong

    Chris KongChris Kong

    Author Reply

    Typo on this line of code:

    if (navigationAction.navigationType == WKNavigationType.LinkActivated && !navigationAction.request.URL.host!.lowercaseString.hasPrefix(“www.appcoda.com”)) {

    I checked the final file attached. It should be:

    if (navigationAction.navigationType == WKNavigationType.LinkActivated && !navigationAction.request.URL.host!.lowercaseString.hasPrefix(“www.appcoda.com”)) {


    • Joyce Echessa

      Some characters in the code blocks were getting changed to html entity codes. I’ve fixed this. Thanks for letting me know.


  • JFM

    JFMJFM

    Author Reply

    Thanks Joyce for a great tutorial!

    There are a couple of things that needs to be updated in the text above to make it work.

    As Chris Kong pointed out there is a typo, but there is also a typo in the row right before that row. The -> has been turned into a HTML Ampersand Character Code, so those two rows should instead be:

    func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {

    if (navigationAction.navigationType == WKNavigationType.LinkActivated && !navigationAction.request.URL.host!.lowercaseString.hasPrefix(“www.appcoda.com”)) {

    To make the book section disappear as described in the text, the second row in hideSections.js needs to changed from:

    styleTag.textContent = ‘div#sidebar, .after-post.widget-area {display:none;}’;

    to:

    styleTag.textContent = ‘div#sidebar, .footer-widgets {display:none;}’;

    The first JavaScript code in the section Extracting Data From the Web Page, needs to be changed to the following, as several characters have turned into HTML Ampersand Char Codes:

    var postsWrapper = document.querySelector(‘#content’)
    var posts = postsWrapper.querySelectorAll(‘.post.type-post.status-publish’)

    for (var i = 0; i < posts.length; i++) {
    var post = posts[i];
    var postTitle = post.querySelector('h2.entry-title a').textContent;
    var postURL = post.querySelector('h2.entry-title a').getAttribute('href');
    console.log("Title: ", postTitle, " URL: ", postURL);
    }

    The same goes for the getPosts.js where the rows containing HTML Ampersands Char Codes need to be replaced as follows:

    for (var i = 0; i < posts.length; i++) {
    var post = posts[i];
    var postTitle = post.querySelector('h2.entry-title a').textContent;
    var postURL = post.querySelector('h2.entry-title a').getAttribute('href');
    pos.push({'postTitle' : postTitle, 'postURL' : postURL});
    }

    The curly bracket is missing on the row:

    class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {

    It might be worth mentioning that when updating the class declaration to conform to the WKScriptMessageHandler protocol, an error will occur that the ViewController does not conform to the protocol, but that we will fix that in a bit by adding the required userContentController method.

    The init in the Post class needs to be changed from:

    init(dictionary: Dictionary) {

    to:

    init(dictionary: Dictionary) {

    The userContentController method needs to be updated from:

    if let postsList = message.body as? [Dictionary] {

    to:

    if let postsList = message.body as? [Dictionary] {

    It might be worth mentioning that it is the id for the Table View Cell that should be updated when talking about the prototype cell.

    Some rows in the PostsTableViewController class has to be updated as several characters have turned into HTML Ampersand Char Codes as follows:

    Change:

    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {

    to:

    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {

    Change:

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {

    to:

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {

    Change:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    to:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    Otherwise it should work 😉


    • Joyce Echessa

      I’ve fixed the code blocks that contained html entity codes. Thanks.


      • Micheal M

        Micheal MMicheal M

        Author Reply

        hello joyce thnks for the great tutorial.
        I have removed the footer from my website but can I modify the HTML code?
        Can you please help me in this


  • Michael Leveton

    I translated this to Objective-C here: https://github.com/Leveton/MELCoda


  • Rob Bishop

    Rob BishopRob Bishop

    Author Reply

    Joyce,

    I know it’s been almost a year since you wrote this tutorial but I just wanted to say a big thank you to you for doing such an excellent job. I’ve been trying to find a way to control video playback speed of an embedded player & after trying various ways could not make it happen. I think that using your method of injecting JavaScript will make it possible. Thanks again & Happy New Year for 2016. RB


  • Steven

    StevenSteven

    Author Reply

    Excellent tutorial! One problem I had with Xcode 7.2 was getting the webView:decidePolicyForNavigationAction method to work. The given code mixed handling of Booleans and Optionals, so I had to modify it to this (which worked):

    func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {

    let userClickedLink = (navigationAction.navigationType == WKNavigationType.LinkActivated)

    if userClickedLink {

    if let host = navigationAction.request.URL?.host {

    let isExternalLink = !(host.lowercaseString.hasPrefix(“www.appcoda.com”))

    if isExternalLink {

    UIApplication.sharedApplication().openURL(navigationAction.request.URL!) // …transfer this url to Safari (via openURL)

    decisionHandler(WKNavigationActionPolicy.Cancel) // …and cancel the page load in this app

    }

    }

    }

    decisionHandler(WKNavigationActionPolicy.Allow)

    }


  • Micheal M

    Micheal MMicheal M

    Author Reply

    hello
    Thanks for the tut
    Modifying the web page content is not working can you please help me how to exclude some text from a webpage using webkit


  • Jan Warner

    Jan WarnerJan Warner

    Author Reply

    This article was really useful. I thought I should go ahead and post a token of my appreciation.


  • 김정수

    김정수김정수

    Author Reply

    great. how can I know what keys (for KVO) are?
    I only have known title, estimated and so on.

    thanks


  • divya.nayak

    Its not working for iOS 11. If I take data from didPost.js which contains the logic that generates the post list it doesnt work. Sending simple dictionary will call userContentController(_ userContentController:, didReceive message:). But it wont be called when there is for loops to get posts.


Shares