iOS

A Look at the WebKit Framework – Part 1


If you’ve ever built an application that required a webview to load web content in your native app, then you have probably experienced the frustrations that came with using UIWebView. UIWebView is quite limited and its performance lags behind that of Mobile Safari. JavaScript, which powers just about every web application, performs poorly in native mobile applications.

However, all of this has changed with the introduction of the WebKit framework in iOS 8. With the WebKit framework, comes WKWebView which replaces UIWebView in UIKit and WebView in AppKit, offering a consistent API across the two platforms.

The WebKit framework enables developers to create web enabled native apps that can utilize the power and Speed of Nitro, which is the JavaScript engine that powers Safari.

webkit-featured

WKWebView boasts of 60fps smooth scrolling, KVO, built-in gestures and native communication between app and webpage.

In the span of two articles, we’ll build two applications which will explore some of the features that WebKit (in particular, WKWebView) offers. For the first application (built in this tutorial), we’ll create a web browser that has some of the functionalities of Safari. The second article will go deeper into Webkit and show the more powerful features like injecting JavaScript into a web page to modify content and extract data.

Getting Started

To get started, create a new project. Make it a Single View Application named Browser and select Swift as the language and make it Universal.

image01

In ViewController.swift import the WebKit framework.

Next add the following variable to the class.

Add the following method to the class. It initializes the web view with frame of size zero. Later we’ll add auto layout constraints to the webview so that it adapts to whichever device and orientation it is run on.

At the bottom of viewDidLoad() add the following statement which will add the webView to the main View.

Next add the following constraints at the bottom of viewDidLoad()

Here we first disable auto-generated constraints with the first statement and then we define the height and width constraints for the webView. The webView will have the same height and width as its superview’s height and width.

We’ll load a default URL when the app starts. Later we’ll add a text field that the user can use to enter a URL. Add the following at the bottom of viewDidLoad()

Run the application. It will load the homepage of Appcoda. When you scroll down, notice that you can see the page scrolling underneath the translucent navigation bar. We’ll disable this. Open Main.storyboard and select the View Controller. In the Attributes Inspector, uncheck the Under Top Bars option in the Extend Edges group. When you run the app again, the nav bar should now be solid and you won’t see the webpage underneath it.

image02

Next we’ll enable url input from the user.

In the storyboard file, drag a view onto the navigation bar in the view controller. In the Attribute Inspector, set its Background to Clear Color. Since you can’t apply auto layout constraints within a navigation bar, we’ll set its size in code.

Open the Assistant Editor and create an outlet for the view by control-dragging from the view to the ViewController class. Name the outlet barView. You should have the following in your code.

Add the following to viewDidLoad() after the call to super.viewDidLoad()

This sets the size of the barView when the app loads.

Add the following method to the class.

This will set the size of the barView when the device orientation changes.

On running the app you should see the view stretched out on the navigation bar. When you change orientations or devices, the view adapt its size accordingly.

Next drag a Text Field onto the view. Using the Pin button at the bottom of the canvas, pin its top, bottom, right and left with a spacing of 0.

image03

Fix the Auto Layout issues by selecting Editor > Resolve Auto Layout Issues > Selected View > Update
Frames

Create an outlet for the text field. Name it urlField. You should have the following.

We want the view controller to receive UITextFieldDelegate protocol methods, so in the Document Outline, control-drag from the text field to the view controller and select delegate from the popup.

image04

With the text field selected, set the following in the Attributes Inspector.

  • Clear Button: Appears while editing
  • Correction: No
  • Keyboard Type: URL
  • Return Key: Go

Change the class declaration as follows so that it conforms to the UITextFieldDelegate protocol.

Next add the following UITextFieldDelegate protocol method to the class.

This dismisses the keyboard and loads the URL given by the user. Test it with a url. You have to enter the full url, i.e. http://google.com. Since this can be a bit cumbersome to your users, you could write code that checks the url string for ‘http://’ and if not present, appends it to the beginning of the url string, thus allowing users to enter such urls as google.com. We won’t get into this here.

Navigating Through History

Our browser is working but still lacks some features that we’ve come to expect of web browsers, namely loading indicator, back and forward buttons, reload button e.t.c.

With KVO (Key Value Observing) loading progress, page title and URL are now observable properties of WKWebView. You can use these values to update your UI accordingly.

First, we’ll add the back, forward and reload buttons.

In the storyboard, select View Controller and then in the Attributes Inspector, under Simulated Metrics, change Bottom Bar to None.

Drag a Toolbar onto the view and place it at the bottom. Pin its left, right and bottom with a spacing of 0, making sure Constrain to margins is unchecked.

image05

In viewDidLoad() edit the webView’s height constraint to take this into account.

Remove the existing button item from the toolbar and drag the following in order: a Bar Button Item, a Fixed Space Bar Button Item, a Bar Button Item, Flexible Space Bar Button Item and a Bar Button Item. The toolbar should look as shown.

image06

Edit the bar button items text to and R respectively. These will be our Back, Forward and Reload buttons. In a real app, it would be better to use icons on the buttons, but for ease, we’ll use text. The toolbar should look as shown.

image07

Create outlets for each of the bar button items. Name them backButton, forwardButton and reloadButton respectively. You should have the following in your code.

Then create actions for the same buttons and name them back, forward and reload respectively. For each action, change the Type to UIBarButtonItem. You should have the following in your code.

At the bottom of viewDidLoad() add the following. We don’t want the back and forward buttons to be enabled when the app first loads.

Add the following to viewDidLoad() after the constraints are added, and before the code that creates and loads a request. This adds the class as an observer of the loading property.

Add the following method to the class. It will be called whenever the observable property changes. The state of the back and forward buttons will be changed according to the current state of the web view.

Modify the back(), forward() and reload() functions as shown.

Run the application and test the buttons. The back and forward should be disabled at first. When you navigate to a page, the back button should be enabled. When you go back, the forward button should be enabled. Tapping R should reload the page.

Handling Errors

You can’t always rely on the user to always type in a correct url. We’ll write code to catch errors and notify the user.

First modify the class declaration as shown.

The WKWebView has a property named navigationDelegate which expects an object that conforms to the WKNavigationDelegate protocol. The protocol provides different methods dealing with navigation events, including loading errors.

Add the following to the bottom of init(). With this, the class will be the navigation delegate of the web view.

Next add the following method to the class. This is the delegate method that gets called when there is an error.

Run and test the app with an incorrect url.

image08

Displaying Progress

To finish off, we’ll add a progress indicator to the app.

In the storyboard file, drag a Progress View onto the view and place it below the navigation bar. Pin its top, right and left as shown.

image09

Create an outlet for the Progress View and name it progressView. You should have the following.

In ViewController, replace the statement

with this

In viewDidLoad(), add the following below the statement that calls addObserver() on the webView, before the creation and loading of the url request.

Add the following to the bottom of observeValueForKeyPath() after the other if statement.

This updates the progress view as the value of estimatedProgress changes or hides it when loading completes.

Add the following method to the class. It is a WKNavigationDelegate protocol method that gets called when the page load completes. We use it here to reset the progress view value after each request.

Run the app and you should see a blue progress line as the app loads.

image10

Conclusion

We have looked at the basics of the new WebKit framework. We’ve seen how to add some of the features offered by Safari, namely loading urls, navigating through history, detecting and handling errors and displaying progress. In part 2 of this tutorial, we’ll go deeper into webview and look at how to inject JavaScript into a web page to build a more powerful application.

You can download the code to this tutorial here.

Tutorial
Building a Message Sticker App Extension in Xcode 8
iOS
Building a Text to Speech App Using AVSpeechSynthesizer
Tutorial
Introduction to the Accelerate Framework in Swift
  • JFM

    JFMJFM

    Author Reply

    Thanks Joyce for for a great tutorial.

    There are a couple of things that need to be corrected in the text as follows:

    Right in the beginning, directly after creating a Single View Application project, you need to select the Main.storyboard, select the View Controller and then go to Editor > Embed In > Navigation Controller.

    Please also note that as the barView Background is set to Clear Color, it will not, as mentioned in the text, be possible to see the view adapt its size when changing device orientation. That will only be possible to observe after adding the text field.

    There is a missing curly bracket on the updated line:
    class ViewController: UIViewController, UITextFieldDelegate {

    For the textFieldShouldReturn, the -> has been turned into a HTML Ampersand Character Codes, so that row should instead be:
    func textFieldShouldReturn(textField: UITextField) -> Bool {

    In the Navigation Through History section, a new view has to be added to the ViewController before a toolbar is dragged onto that view.

    The are missing in the text “Edit the bar button items text and R respectively.”

    The context parameter for the observeValueForKeyPath method is incorrect and should be:
    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {

    The curly bracket is missing on the row:
    class ViewController: UIViewController, UITextFieldDelegate, WKNavigationDelegate {

    Otherwise it should work 🙂


    • Novice

      NoviceNovice

      Author Reply

      Thanks for the tip to embed the view to navigation. This had me stumped for a bit.


  • Hemanthgk10

    Thanks for the awesome tutorial.

    I would like to know how to implement this is Objective-C as well. Thanks in advance.


  • James

    JamesJames

    Author Reply

    Thank you for this tutorial. I’ve always disliked how many browsers do not offer the option to start with a blank page but insist on loading the last visited page. Now I can build my own custom browser suitable to my personal needs.


  • Jonathan

    JonathanJonathan

    Author Reply

    I couldn’t get the keyboard to hide until I added self.urlField.delegate = self to viewDidLoad().


  • Maddoggy1979

    There’s a slight error in your observeValueForKeyPath function. The UnsafeMutablePoint is missing the correct brackets. It should be as below, and is correct in the downloaded finished code example

    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer <<< missing the


  • taande

    taandetaande

    Author Reply

    Hi, so i am following this tutorial to convert a web app that is built in google dart to an ipad app. so copied all the code in this tutorial and followed the steps. it works if i let url = NSURL(string:”http://google.com”) but it wont work with the url for my app. on the ios simulator that i am running on xcode, i just get a blank white screen. Do you know what i can do? Thank you


    • Dan Beaulieu

      Not sure if this is the issue but, if you’re on local intranet you’ll need to use an IP address rather than intranet.whatever or localhost


  • csru

    csrucsru

    Author Reply

    XCode isn’t letting me drop a View onto the Navigation bar. So I am not able to get the view hierarchy that you have in the screenshot.


    • George Campbell

      On the bottom right side, you must select “Show the Object Library” (icon is a square inside a circle). You are probably trying to drop a view with “Show the File Template library” selected.


  • Wdot

    WdotWdot

    Author Reply

    How to replace textfield with uisearchbar?


  • Mark Beaty

    Mark BeatyMark Beaty

    Author Reply

    Excellent tutorial on WKWebView. One other issue: you need to remove your observers when the page is unloaded (or in deinit). Otherwise, it will crash.


  • Koen van der Drift

    Don’t forget to remove the observers in viewWillDisappear


  • Mark Lucking

    Another excellent post; just ran it on IOS 9.0 with Xcode 7.0. Xcode fixed most of the code, just needed to post this little gem into the plist to get it to work!

    NSAppTransportSecurity

    NSAllowsArbitraryLoads

    Good work AppCoda!!


  • Vincenzo

    VincenzoVincenzo

    Author Reply

    Hi,

    do you have any tip for managing the navigation History? You mentioned it in the title, but I couldn’t find any tip for that in the article…


  • Herbert Perryman

    this tutorial no longer works due to app transport security update. please update the app


  • George Campbell

    In addition to the fix for NSAppTransportSecurity below, I had to make 2 changes to make it work:

    // XCode got religion about chaining failable to non-failable methods – add a ! after this line.
    – super.init(coder: aDecoder)
    + super.init(coder: aDecoder)!

    // The autoresizing mask was changed from a method to a property, so replace this line.
    – webView.setTranslatesAutoresizingMaskIntoConstraints(false)
    + webView.translatesAutoresizingMaskIntoConstraints = false;

    Outstanding work Joyce – this really got me started quickly.


  • JAMESMAC

    JAMESMACJAMESMAC

    Author Reply

    Hmm. I made the required changes and updated to utilise Swift 2.0. The app will build no worries but will not make a URL request… I just get blank black screen. Any tips?


  • sangmin

    sangminsangmin

    Author Reply

    hi

    I made webview via WKWeb. But, in this picture there are something wrong. All source is bottom part of the picture. It intruding upper portion bar I don’t know which code I have to modify. Please let me know it.

    //

    // ViewController.swift

    // barojava

    //

    // Created by macbook on 2016. 2. 11..

    // Copyright © 2016년 superface. All rights reserved.

    //

    import UIKit

    import WebKit

    class ViewController: UIViewController, UIWebViewDelegate, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate{

    var webView: WKWebView!

    var HomeUrl = “http://”

    var busPhone = “01”

    @IBOutlet weak var backButton: UIBarButtonItem!

    @IBOutlet weak var forwardButton: UIBarButtonItem!

    @IBOutlet weak var reloadButton: UIBarButtonItem!

    @IBOutlet weak var progressView: UIProgressView!

    @IBOutlet weak var PhoneCallJava: UIBarButtonItem!

    //자바스크립트 부릿지 WKWeb 설정부분

    required init?(coder aDecoder: NSCoder) {

    super.init(coder: aDecoder)

    let contentController = WKUserContentController();

    //앱에서 -> 웹으로 보낼때

    let userScript = WKUserScript(

    source: “redHeader()”,

    injectionTime: WKUserScriptInjectionTime.AtDocumentEnd,

    forMainFrameOnly: true

    )

    contentController.addUserScript(userScript)

    contentController.addScriptMessageHandler(

    self,

    name: “call” // 클라이언트 페이지 자바스크립트 네임

    )

    let config = WKWebViewConfiguration()

    config.userContentController = contentController

    self.webView = WKWebView(frame: CGRectZero, configuration: config) // 브리지변수추가 configuration: config

    self.webView.navigationDelegate = self //웹뷰자바스크립트경고창처리 꼭필요.WKUIDelegate 클래스선언과 함께

    }

    override func viewDidLoad() {

    super.viewDidLoad()

    //상단프로그래스바

    view.insertSubview(webView, belowSubview: progressView)

    //WKWeb 레이아웃 설정부분

    webView.translatesAutoresizingMaskIntoConstraints = false

    let height = NSLayoutConstraint(item: webView, attribute: .Height, relatedBy: .Equal, toItem: view, attribute: .Height, multiplier: 1, constant: -44 )

    let width = NSLayoutConstraint(item: webView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0)

    view.addConstraints([height, width])

    //print(UIApplication.sharedApplication().statusBarFrame.size.height)

    //앞/뒤 활성화코드

    webView.addObserver(self, forKeyPath: “loading”, options: .New, context: nil)

    webView.addObserver(self, forKeyPath: “estimatedProgress”, options: .New, context: nil)

    let url = NSURL(string:HomeUrl)

    let request = NSURLRequest(URL:url!)

    webView.loadRequest(request)

    //초기 앞뒤 비활성화 시키기

    backButton.enabled = false

    forwardButton.enabled = false

    self.webView.UIDelegate = self

    //appDelegate의PushReceived 함수를받는다.

    NSNotificationCenter.defaultCenter().addObserver(self, selector:”PushReceiver:”, name: “PushReceived”, object: nil)

    }

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {

    }

    //웹뷰관련 액션

    @IBAction func back(sender: UIBarButtonItem) {

    webView.goBack()

    }

    @IBAction func forward(sender: UIBarButtonItem) {

    webView.goForward()

    }

    @IBAction func reload(sender: UIBarButtonItem) {

    let request = NSURLRequest(URL:webView.URL!)

    webView.loadRequest(request)

    }

    @IBAction func goHome(sender: AnyObject) {

    let url = NSURL(string:HomeUrl)

    let request = NSURLRequest(URL:url!)

    webView.loadRequest(request)

    }

    @IBAction func phoneCall(sender: AnyObject) {

    if let url = NSURL(string: “tel://(busPhone)”) {

    let cancelation = UIAlertAction(title: “취소”, style: UIAlertActionStyle.Default){(ACTION) in

    print(“취소버튼누름”)

    }

    let myAlert = UIAlertController(title: “고객센터”, message: “전화 연결 하시겠습니까?”, preferredStyle: UIAlertControllerStyle.Alert)

    let okAction = UIAlertAction(title: “확인”, style: UIAlertActionStyle.Default){(ACTION) in

    print(“전화버튼누름”)

    UIApplication.sharedApplication().openURL(url)

    }

    myAlert.addAction(okAction)

    myAlert.addAction(cancelation)

    self.presentViewController(myAlert, animated: true, completion: nil)

    }

    }

    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) {

    if (keyPath == “loading”) {

    backButton.enabled = webView.canGoBack

    forwardButton.enabled = webView.canGoForward

    }

    if (keyPath == “estimatedProgress”) {

    progressView.hidden = webView.estimatedProgress == 1

    progressView.setProgress(Float(webView.estimatedProgress), animated: true)

    }

    }

    func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) {

    let alert = UIAlertController(title: “Error”, message: error.localizedDescription, preferredStyle: .Alert)

    alert.addAction(UIAlertAction(title: “Ok”, style: .Default, handler: nil))

    presentViewController(alert, animated: true, completion: nil)

    }

    func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {

    progressView.setProgress(0.0, animated: false)

    }

    func PushReceiver(notifi: NSNotification )

    {

    //appDelegate 에서 userInfo 정보를 ViewController 으로가져온다.

    let userInfo = notifi.userInfo

    //apn.php 파일에서 파라메터 수신

    let aps = userInfo![“aps”] as? NSDictionary

    let pageLink = aps![“pagelink”] as? NSString

    let message = aps![“alert”] as? String // 스트링선언

    print(pageLink)

    let myAlert = UIAlertController(title: “알림”, message: message, preferredStyle: UIAlertControllerStyle.Alert)

    let okAction = UIAlertAction(title: “닫기”, style: UIAlertActionStyle.Default){(ACTION) in

    print(“닫기버튼누름”)

    }

    let cencelAction = UIAlertAction(title: “보기”, style: UIAlertActionStyle.Cancel){(ACTION) in

    //공지뷰컨트롤러 띄우는부분

    /* itemViewController.swift를 스토리보드 id 설정없이 코드로 띄우는부분,메인스토리보드 이름 “Main” */

    let storyboard = UIStoryboard(name: “Main”, bundle: nil)

    //itemViewSid 는itemViewController 의 스토리보드 ID다.

    let controller = storyboard.instantiateViewControllerWithIdentifier(“itemViewSid”) as! itemViewController

    //받는페이지의 receivedURL 변수형이 string로되어있어야 오류가 나질않는다.

    controller.receivedURL = “barojava/push_view.php?page=(pageLink)” //푸쉬에서받은 파라메터전달

    self.presentViewController(controller, animated: true, completion: nil)

    }

    myAlert.addAction(okAction)

    myAlert.addAction(cencelAction)

    self.presentViewController(myAlert, animated: true, completion: nil)

    }

    //alert

    //자바스크립트브릿지 webPageExm: webkit.messageHandlers.call.postMessage(“viewpage->barojava/push_view.php”);

    func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {

    if(message.name == “call”) {

    //파라메터 상수 설정 아래 공용으로 사용

    let page = String (message.body)

    //스키마변수명의 문자열 길이를 구한다. 스키마변수명이달라질경우를 대비하여 앞에서부터 문자열길이 만큼 잘라서 뒤에 변수사용.

    let range = page.rangeOfString(“->”)

    //페이지url

    let b = range!.endIndex

    let pageUrl = page.substringFromIndex(b) //최종 서브URL추출 변수

    //페이지 변수명

    let nameVal = range!.startIndex

    let namepage = page.substringToIndex(nameVal) //최종 변수

    print(“변수명:(namepage)”)

    print(“페이지주소:(pageUrl)”)

    //클라이언트와 통신을 통해 유동적으로 페이지 이동 변수명과파라메터를 나눈다.

    if namepage == “viewpage” {

    /* itemViewController.swift를 스토리보드 id 설정없이 코드로 띄우는부분,메인스토리보드 이름 “Main” */

    let storyboard = UIStoryboard(name: “Main”, bundle: nil)

    //itemViewSid 는itemViewController 의 스토리보드 ID다.

    let controller = storyboard.instantiateViewControllerWithIdentifier(“itemViewSid”) as! itemViewController

    //받는페이지의 receivedURL 변수형이 string로되어있어야 오류가 나질않는다.

    controller.receivedURL = pageUrl

    self.presentViewController(controller, animated: true, completion: nil)

    }

    if namepage == “pushpage”{

    /* itemViewController.swift를 스토리보드 id 설정없이 코드로 띄우는부분,메인스토리보드 이름 “Main” */

    let storyboard = UIStoryboard(name: “Main”, bundle: nil)

    //itemViewSid 는itemViewController 의 스토리보드 ID다.

    let controller = storyboard.instantiateViewControllerWithIdentifier(“itemViewSid”) as! itemViewController

    //받는페이지의 receivedURL 변수형이 string로되어있어야 오류가 나질않는다.

    controller.receivedURL = pageUrl

    self.presentViewController(controller, animated: true, completion: nil)

    }

    }

    }//자바스크립트 브릿지 함수끝

    override func didReceiveMemoryWarning() {

    super.didReceiveMemoryWarning()

    // Dispose of any resources that can be recreated.

    }

    } //END CLASS

    //자바스크립트 기본 얼렛창->WKWEBview는 기본 얼렛을 처리해줘야함.

    private typealias wkUIDelegate = ViewController

    extension wkUIDelegate {

    func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: () -> Void) {

    let ac = UIAlertController(title: webView.title, message: message, preferredStyle: UIAlertControllerStyle.Alert)

    ac.addAction(UIAlertAction(title: “OK”, style: UIAlertActionStyle.Cancel, handler: { (aa) -> Void in

    completionHandler()

    }))

    self.presentViewController(ac, animated: true, completion: nil)

    }

    }


  • Nadim

    NadimNadim

    Author Reply

    Thanks for tut. I have been developing iOS apps for about 4 years & I have no knowledge of web developing or Developing career. & I am still at school( last years). My question is that is developing for me? I enjoy programming since I was a kid. But I a developer may only have job not career.


  • erikandershed

    Great tutorial! Do you know how to get this browser to play video inline?


  • Grant Kemp

    Grant KempGrant Kemp

    Author Reply

    Looks interesting sadly this seems broken for Swift 3. I will have a go at updating it.


  • Mahdi Habash

    Hi appcoda ,
    Thanks for great tutorial
    I have a question it is possible if app shows wkwebview to call native app or in other way call another view controller ..
    If possible can you provide me with a sample code or tutorial
    thanks in advance


  • Kamlesh Jha

    ‘CGRectZero’ is unavailable in Swift. Swift 4


Leave a Reply to taande
Cancel Reply

Shares