Creating Simple Web Browser with WKWebView & UINavigationController
Tags: ios, swift, wkwebview, swiftui
Published: 2020-03-15 08:46:46

Table of contents
  - [Setup Previews for your project](#setup-previews-for-your-project)
  - [Your first WebView](#your-first-webview)
  - [Adding title](#adding-title)
    - [Using JavaScript](#using-javascript)
    - [Using Key-Value Observing](#using-key-value-observing)
  - [Back, forward, reload](#back-forward-reload)
    - [State of the buttons](#state-of-the-buttons)
  - [Progress bar](#progress-bar)

`WKWebView` was first introduced on iOS 8. With Apple finally release [a deadline](https://developer.apple.com/news/?id=12232019b) for all apps to migrate away from `UIWebView`, this series and this post is here to help you explore the features of `WKWebView`. In this blog post you will create a simple web browser with some basic features such as displaying content, back and forward.

## Setup Previews for your project

One of the most interesting things coming out with Xcode 11 is `SwiftUI`'s `PreviewProvider`, which provides a way to preview the UI during development **instantly** on multiple devices, multiple settings at the same time. 

To preview `UIViewController` and `UIView`, you need to download previewing code from [NSHipster](https://gist.github.com/mattt/ff6b58af8576c798485b449269d43607)

Since we are making this browser with a navigation bar, our main `UIViewController` needs to be embedded inside a `UINavigationController`. Therefore the previewing code would be like this: 

```swift
func previewWithNavigationController(_ webViewController: UIViewController) -> some View {
    UIViewControllerPreview {
        let n = UINavigationController()
        n.pushViewController(webViewController, animated: true)
        return n
    }
}
```

## Your first WebView

Importing what is needed:

```swift
import WebKit
```

Initializing a `WKWebView` instance and add it to our main `UIViewController`

```swift
class Browser: UIViewController {
    var webView = WKWebView()
    override func viewDidLoad() {
        self.view.addSubview(webView)
    }
}
```

Inside `viewDidLoad`, setup AutoLayout for our web view:

```swift
self.webView.translatesAutoresizingMaskIntoConstraints = false
self.webView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
```

Loading a specific web link at start:

```swift
self.webView.load(URLRequest(url: URL(string: "https://www.google.com")!))
```

Finally, complete our Preview code to get the result in the GIF. (we're using `Group` to display multiple SwiftUI `View`)

```swift
struct BrowserPreview: PreviewProvider {
    static var previews: some View {
        Group {
            previewWithNavigationController(Browser())
        }
    }
}

```

![step 1. Simple WKWebView](https://media.giphy.com/media/U6e9CBiF9CvKJiIVMW/giphy.gif)

(in my example, I set WKWebView background to pink color to clearly display it on preview)

## Adding title

Every web page has a title. And there's two ways to find this information and display on our navigation bar:

### Using JavaScript

This technique is also a traditional way to get web page title on `UIWebView`. `webView.stringByEvaluatingJavaScript(from: "document.title")` is your needed code. However, to get updates on title changes, we need to conform to `WKNavigationDelegate`:

```swift
/// inside `viewDidLoad`
self.webView.navigationDelegate = self
```

```swift
extension Browser: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.webView.evaluateJavaScript(
            "document.title"
        ) { (result, error) -> Void in
            self.navigationItem.title = result as? String
        }
    }
}
```

### Using Key-Value Observing

KVO is an Objective-C feature that help you to track changes of any properties of a `NSObject`. Property `title` is what we need: 

Observe the web view:

```
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: .new, context: nil)
```

update the navigation title with the change:

```swift
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(WKWebView.title) {
            self.navigationItem.title = self.webView.title
        }
    }
```

![](https://media.giphy.com/media/VhFETSyFBRc0I5mVY9/giphy.gif)

## Back, forward, reload 

Instead of using external images for those buttons, I will use SF Symbols, introduced with iOS 13 and Xcode 11, as button icon. 

Let's preview one of these first, with tint color

```swift
struct BrowserResourcePreview: PreviewProvider {
    static var previews: some View {
        Group{
            UIViewControllerPreview {
                let n = UINavigationController()
                let v = UIViewController()
                let img = UIImage(systemName: "arrow.left")!.withTintColor(.blue, renderingMode: .alwaysTemplate)
                v.navigationItem.setLeftBarButton(UIBarButtonItem(image: img, style: .plain, target: nil, action: nil), animated: true)
                n.pushViewController(v, animated: true)
                return n
            }
        }
    }
}
```

![](https://i.imgur.com/WLNrhjom.png)

Similarly, we use `arrow.right` for the forward button and `arrow.counterclockwise` for refresh button. After using navigation bar for title, we will use toolbar for those buttons

On feature side, `WKWebView` provides methods: `goBack`, `goForward`, `reload`, which are perfect for what we need

```swift
    var backButton: UIBarButtonItem?
    var forwardButton: UIBarButtonItem?
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationController?.setToolbarHidden(false, animated: true)
        let backButton = UIBarButtonItem(
            image: UIImage(systemName: "arrow.left")!.withTintColor(.blue, renderingMode: .alwaysTemplate),
            style: .plain,
            target: self.webView,
            action: #selector(WKWebView.goBack))
        let forwardButton = UIBarButtonItem(
            image: UIImage(systemName: "arrow.right")!.withTintColor(.blue, renderingMode: .alwaysTemplate),
            style: .plain,
            target: self.webView,
            action: #selector(WKWebView.goForward))
        let reloadButton = UIBarButtonItem(
                   image: UIImage(systemName: "arrow.counterclockwise")!.withTintColor(.blue, renderingMode: .alwaysTemplate),
                   style: .plain,
                   target: self.webView,
                   action: #selector(WKWebView.reload))
        
        self.toolbarItems = [backButton, forwardButton,
                             UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
                             reloadButton
        ]
        self.backButton = backButton
        self.forwardButton = forwardButton
    }
```

![](https://media.giphy.com/media/L0kCE874pFGrzXVP9M/giphy.gif)

### State of the buttons

Besides, to make our UI more intuitive, we need to display when the web view can go back or go forward. This time, we use KVO again with 2 properties: `canGoBack`, `canGoForward`.

```swift
override func viewDidLoad() {
    super.viewDidLoad()
    self.backButton?.isEnabled = self.webView.canGoBack
    self.forwardButton?.isEnabled = self.webView.canGoForward
    self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: .new, context: nil)
    self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    if let _ = object as? WKWebView {
        if keyPath == #keyPath(WKWebView.canGoBack) {
            self.backButton?.isEnabled = self.webView.canGoBack
        } else if keyPath == #keyPath(WKWebView.canGoForward) {
            self.forwardButton?.isEnabled = self.webView.canGoForward
        }
    }
}
```

![](https://media.giphy.com/media/MCKzh1zs0Lf2CxeDXV/giphy.gif)

## Progress bar

Adding a progress bar, also with KVO this time. On UI, we also need to add tint color and make a larger easy-to-see progress bar

```swift
// adding progress view
let progressView = UIProgressView(progressViewStyle: .default)
self.progressBar = progressView
self.view.addSubview(progressView)
// updating auto layout & UI
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1.0).isActive = true

if #available(iOS 11.0, *) {
    progressView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
}
progressView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
progressView.setProgress(0.0, animated: true)
progressView.transform = progressView.transform.scaledBy(x: 1, y: 4)
progressView.backgroundColor = .gray
progressView.tintColor = .blue
```

Observing the `estimatedProgress`:

```swift
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
```

```swift
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        if let o = object as? WKWebView, o == self.webView {
            if keyPath == #keyPath(WKWebView.estimatedProgress) {
                progressBar?.setProgress(Float(self.webView.estimatedProgress), animated: true)
            }
        }
}
```

![](https://media.giphy.com/media/gGrMyqGMBUrK8tlhKT/giphy.gif)

Take a look at the GIF above, we can see in some cases, the progress bar is going backwards and it should hide after web view finish the loading. We can update the observing code above:

```swift
if keyPath == #keyPath(WKWebView.estimatedProgress), let progressView = self.progressBar {
    let newProgress = self.webView.estimatedProgress
    if Float(newProgress) > progressView.progress {
        progressView.setProgress(Float(newProgress), animated: true)
    } else {
        progressView.setProgress(Float(newProgress), animated: false)
    }
    if newProgress >= 1 { // delaying so that user can see progress view reach 100%
        DispatchQueue.main.asyncAfter(deadline: .now()+0.3, execute: {
            progressView.isHidden = true
        })
    } else {
        progressView.isHidden = false
    }
}
```

![](https://media.giphy.com/media/iFsPRkBb6eK5SB0Myz/giphy.gif) 


## Conclusion

With this tutorial, we explored the basic features of `WKWebView`, and also combining powerful features from **SwiftUI**, Objective-C, UIKit to build a simple web browser.

You can find the full source code for this tutorial [here on GitHub Gist](https://gist.github.com/quangDecember/9a13c28619f1db67aa3a5cfbb5936049)

Thanks for reading 🙏 and feel free to leave me any comments or questions