DEV Community

Quang
Quang

Posted on • Updated on

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

Your first WebView

Importing what is needed:

import WebKit

Initializing a WKWebView instance and add it to our main UIViewController

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

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

Loading a specific web link at start:

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)

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

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

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

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

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
    }

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

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

Observing the estimatedProgress:

self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), 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 o = object as? WKWebView, o == self.webView {
            if keyPath == #keyPath(WKWebView.estimatedProgress) {
                progressBar?.setProgress(Float(self.webView.estimatedProgress), animated: true)
            }
        }
}

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

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

Latest comments (2)

Collapse
 
complabs profile image
Comp Labs

Please share .xcodeproj file and App folder in Github please

Collapse
 
yamanwa profile image
Yaman

Very Nice, a small suggestion though, the reload button can act as a stop button while loading