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.
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
}
}
.
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())
}
}
}
.
(in my example, I set WKWebView background to pink color to clearly display it on preview)
Every web page has a title. And there's two ways to find this information and display on our navigation bar:
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
}
}
}
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
}
}
.
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
}
.
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
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
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
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