Using web view is not new. We had UIWebView
for many years.
Then came WKWebView
in iOS 8, and we had to re-learn some stuff.
This is a complete guide (in Swift!) to implementing your view controller that has a WKWebView
, for the purpose of (duh) displaying webpages.
It is not as simple as I thought it would be, with a couple of lessons I learnt along the way.
Setting up with Interface Builder
Currently (as of Xcode 7.3.1), you cannot add a WKWebView
to your scene directly. There is a web view component in Object Library, but that is for the old UIWebView
.
I am sure the component will be added someday, but for now, you have to setup in your code.
class MyWebViewController: UIViewController {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Create WKWebView in code, because IB cannot add a WKWebView directly
webView = WKWebView()
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-[webView]-|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: ["webView": webView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-20-[webView]-|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: ["webView": webView]))
// 2 ways to load webpage: `loadHTML()` or `loadURL()`
}
}
In viewDidLoad
, we have to create WKWebView
in code.
We add it to the view, flushing to the bounds, using auto layout visual language format.
There are 2 ways to load a webpage:
- Load HTML as String
- Load URL request
In viewDidLoad
, call either loadHTML()
or loadURL()
, which we gonna explore next.
1. Load HTML as String
func loadHTML() {
webView.loadHTMLString("<html><body>"
+ "<p><a href=\"http://samwize.com\">http://samwize.com</a></p>"
+ "</body></html>", baseURL: nil)
}
We use loadHTMLString
to load a local HTML.
Run this now, tap on the link, and nothing happens.
Why?
Because App Transport Security policy requires the use of a secure connection, or unless you whitelist it. Add the following to your Info.plst
:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
How did I know of the error?
Glad you ask.
While Xcode debugger does not show up any error/warning, there is an the awesome Safari Debugger.
Go to Safari > Develper > Simulator/Your Device, and you will be able to debug the page just like on desktop.
For a real device, you will need to enable the feature in Settings app > Safari > Advanced.
Keep in mind of this VERY useful tool.
2. Load URL request
Simply using loadRequest
.
func loadURL() {
let urlString = "http://samwize.com"
guard let url = NSURL(string: urlString) else {return}
let request = NSMutableURLRequest(URL:url)
webView.loadRequest(request)
}
The 2 delegates
There are 2 delegates to the web view which you will most likely use.
class MyWebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
override func viewDidLoad() {
...
webView.UIDelegate = self
webView.navigationDelegate = self
}
}
WKUIDelegate provides the method for presenting some native user interfaces (see Javascript dialog boxes later).
WKNavigationDelegate track navigations from page to page.
Change URL Request HTTP Header
You can change a HTTP header value with a NSMutableURLRequest
eg. in loadURL()
like this:
request.setValue("Wolverine", forHTTPHeaderField: "X-Men-Header")
One of the most common header field is the User-Agent. You can set it on a request object.
But a more convenient way is using webView.customUserAgent
to set just once for the web view. Or you can change it globally by setting NSUserDefaults.
And this is how you find out the current user agent:
webView.evaluateJavaScript("navigator.userAgent") { (result, error) -> Void in
print("User-Agent: \(result)")
}
Go Back/Forward and Progress
You can navigate the history with the web view goBack()
and goForward()
.
To track the progress of loading a webpage, you will need to observe some key paths. Add this in viewDidLoad
let webViewKeyPathsToObserve = ["loading", "estimatedProgress"]
for keyPath in webViewKeyPathsToObserve {
webView.addObserver(self, forKeyPath: keyPath, options: .New, context: nil)
}
Then as the values change:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "loading":
// If you have back and forward buttons, then here is the best time to enable it
backButton.enabled = webView.canGoBack
forwardButton.enabled = webView.canGoForward
case "estimatedProgress":
// If you are using a `UIProgressView`, this is how you update the progress
progressView.hidden = webView.estimatedProgress == 1
progressView.progress = Float(webView.estimatedProgress)
default:
break
}
}
Lastly, when page is loaded, reset the progress view.
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
progressView.setProgress(0.0, animated: false)
}
Print the HTML text
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: { (html: AnyObject?, error: NSError?) in
print(html)
})
Pitfall: Handling Javascript Dialog Boxes
This is often omitted in a custom implementation of WKWebView
, yet it is very important.
Unlike Safari, WKWebView
left out how the 3 Javascript dialog boxes are handled.
You MUST implement the methods in WKUIDelegate
to have a proper working web browser.
Here, we have a simple implementation by using UIAlertController
to show the dialog boxes.
/// Handle javascript:alert(...)
func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: () -> Void) {
...
let okAction = UIAlertAction(title: Okay, style: .Default) { _ in
completionHandler()
}
...
}
/// Handle javascript:confirm(...)
func webView(webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: (Bool) -> Void) {
...
let okAction = UIAlertAction(title: Okay, style: .Default) { _ in
completionHandler(true)
}
let cancelAction = UIAlertAction(title: Cancel, style: .Cancel) { _ in
completionHandler(false)
}
...
}
/// Handle javascript:prompt(...)
func webView(webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: (String?) -> Void) {
...
alertController.addTextFieldWithConfigurationHandler { (textField) in
textField.text = defaultText
}
let okAction = UIAlertAction(title: Okay, style: .Default) { action in
let textField = alertController.textFields![0] as UITextField
completionHandler(textField.text)
}
let cancelAction = UIAlertAction(title: Cancel, style: .Cancel) { _ in
completionHandler(nil)
}
...
}
In the code above, I have left out the not-so-important part of creating UIAlertController, adding the actions, and presenting it.
What’s important are the UIAlertAction
and the completionHandler
to call back with.
Pitfall: Unsupported URL
Custom scheme URL is not supported by WKWebView
(but Safari will work).
You can make it work by handling the “error”:
func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) {
handleError(error)
}
func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) {
handleError(error)
}
func handleError(error: NSError) {
if let failingUrl = error.userInfo["NSErrorFailingURLStringKey"] as? String {
if let url = NSURL(string: failingUrl) {
let didOpen = UIApplication.sharedApplication().openURL(url)
if didOpen {
print("openURL succeeded")
return
}
}
}
}
Note: In iOS 9, you have to whitelist the URL schemes if you use canOpenURL
, therefore we simply go ahead and openURL
, then use the returned boolean.
Bonus: Universal Links
Universal links are http://...
URL that will open an app. They are similar to custom URI scheme to open app, but using regular http addresses. Yet it was claimed to be untested software from Apple.
It is quite a tricky technology.
As noted in Apple Doc, iOS 9 users can tap on universal links in WKWebView
, and it will open the app. It is the same for UIWebView
and SFSafariViewController
.
There is no way you can prevent a universal link from opening an app (if installed) when the link is in a page in your web view. Google Chrome app is the same. It was clarified by their engineer that deep/universal link will still be opened if it results from a user’s tap.
Branch provided a good guide on when universal link will NOT open the app:
- When the link is entered into the browser address field (thus creating the intial web view)
- Same domain
- Javascript
onload()
orclick()
In other words, it only works if the link is cross domain and is user driven (clicking on a <a>
tag).
If a universal link sometimes did not work, then it is likely the OS has remembered your preference for the link. You have to “reset” it.
If you want to exclude a path, you can use a NOT
keyword.