Creating Simple Web Browser with WKWebView & UINavigationController

quangdecember

Quang

Posted on March 15, 2020

Creating Simple Web Browser with WKWebView & UINavigationController

Table of contents

WKWebView was first introduced on iOS 8. With Apple finally release a deadline 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

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:



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


Enter fullscreen mode Exit fullscreen mode

Your first WebView

Importing what is needed:



import WebKit


Enter fullscreen mode Exit fullscreen mode

Initializing a WKWebView instance and add it to our main UIViewController



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


Enter fullscreen mode Exit fullscreen mode

Inside viewDidLoad, setup AutoLayout for our web view:



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


Enter fullscreen mode Exit fullscreen mode

Loading a specific web link at start:



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


Enter fullscreen mode Exit fullscreen mode

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



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



Enter fullscreen mode Exit fullscreen mode

step 1. Simple WKWebView

(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:



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


Enter fullscreen mode Exit fullscreen mode


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
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

update the navigation title with the change:



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
        }
    }


Enter fullscreen mode Exit fullscreen mode

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



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
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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



    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
    }


Enter fullscreen mode Exit fullscreen mode

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.



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
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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



// 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


Enter fullscreen mode Exit fullscreen mode

Observing the estimatedProgress:



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


Enter fullscreen mode Exit fullscreen mode


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)
            }
        }
}


Enter fullscreen mode Exit fullscreen mode

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:



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
    }
}


Enter fullscreen mode Exit fullscreen mode

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

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

💖 💪 🙅 🚩
quangdecember
Quang

Posted on March 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related