Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save munho/35ff7092b52cf189895fcc53f5bc0000 to your computer and use it in GitHub Desktop.
Save munho/35ff7092b52cf189895fcc53f5bc0000 to your computer and use it in GitHub Desktop.
Creating Simple Web Browser with WKWebView & UINavigationController

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

.

i

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
    }

.

i

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

i

x

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)

i

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

i

r

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

i

r

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

//
// WKWebViewNavigatableBrowser+Previews.swift
// PrototypeCocoa
//
// Created by Quang on 3/10/20.
// Copyright © 2020 Quang. All rights reserved.
//
import Foundation
import SwiftUI
struct BrowserPreview: PreviewProvider {
static var previews: some View {
Group {
// previewWithNavigationController(BrowserStep1())
// previewWithNavigationController(BrowserStep2())
// previewWithNavigationController(BrowserStep3())
previewWithNavigationController(BrowserStep4())
// previewWithNavigationController(BrowserStep5())
}
}
}
struct BrowserResourcePreview: PreviewProvider {
static var previews: some View {
Group{
UIViewPreview {
let img = UIImage(systemName: "arrow.left")!.withTintColor(.blue, renderingMode: .alwaysTemplate)
return UIImageView(image: img)
}
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: v, action: #selector(UIViewController.viewDidLoad)), animated: true)
n.pushViewController(v, animated: true)
return n
}
}
}
}
//
// WKWebView+NavigatableBrowser.swift
// PrototypeCocoa
//
// Created by Quang on 3/7/20.
// Copyright © 2020 Quang. All rights reserved.
//
import Foundation
import UIKit
import WebKit
import SwiftUI
class BrowserStep1: UIViewController {
var webView: WKWebView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(webView)
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
self.webView.backgroundColor = .systemPink
self.webView.load(URLRequest(url: URL(string: "https://www.google.com")!))
}
}
class BrowserStep2: BrowserStep1 {
override func viewDidLoad() {
super.viewDidLoad()
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: .new, context: nil)
}
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
}
}
}
class BrowserStep2_2: BrowserStep1{
override func viewDidLoad() {
super.viewDidLoad()
self.webView.navigationDelegate = self
}
}
extension BrowserStep2_2: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView.evaluateJavaScript(
"document.title"
) { (result, error) -> Void in
self.navigationItem.title = result as? String
}
}
}
class BrowserStep3: BrowserStep2 {
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
}
}
class BrowserStep4: BrowserStep3 {
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
}
}
}
}
class BrowserStep5: BrowserStep4 {
var progressBar: UIProgressView?
override func viewDidLoad() {
super.viewDidLoad()
let progressView = UIProgressView(progressViewStyle: .default)
self.progressBar = progressView
self.view.addSubview(progressView)
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
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)
// }
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 {
DispatchQueue.main.asyncAfter(deadline: .now()+0.3, execute: {
progressView.isHidden = true
})
} else {
progressView.isHidden = false
}
}
}
}
}
func previewWithNavigationController(_ viewController: UIViewController) -> some View {
UIViewControllerPreview {
let n = UINavigationController()
n.pushViewController(viewController, animated: true)
return n
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment