Long after WKWebView Hello World – Part 1 – Cookie Handling

First of all, I want to thank my friend and colleague Lucien Dupont for inviting  me to contribute.

Recently our project transitioned web views written in UIWebView to WKWebView and Lucien suggested that I share some of my experiences with the larger community.  The intent here is not a tutorial on how to write web views using WKWebView, (there are tons of great, and free material out there) but suggest solutions on problems that we struggled with and hopefully help those with similar issues.

Before I begin, I am greatly indebted to the ton of postings in Stack Overflow and other web sites.  In particular please bookmark the following links whose authors really took the time to document the many tips they collected over the years:

https://github.com/ShingoFukuyama/WKWebViewTips

http://atmarkplant.com/ios-wkwebview-tips/

Finally my discussions assumes iOS 9 and above, Xcode 8.2.1, Swift 3, basic WKWebView API

Cookie Handling

Adding Cookies

Quick take: Cookies are added using Javascript and only after the document is loaded

Adding cookies to a WKWebView from the app is different from UIWebView.  For UIWebView, one can take advantage of the APIs in HTTPCookieStorage as follow:

let cookieStorage = HTTPCookieStorage.shared
let cookieProperties: [AnyHashable: Any] =
[HTTPCookiePropertyKey.domain: "mydomain",
HTTPCookiePropertyKey.path: "/",
HTTPCookiePropertyKey.name: "mycookie",
HTTPCookiePropertyKey.value: ",abc"]
let cookie = HTTPCookie(properties: cookieProperties as! [HTTPCookiePropertyKey : Any])

cookieStorage.setCookie(cookie!)
UserDefaults.standard.synchronize()

Add a cookie to the HTTPCookieStorage and viola, the cookie is added.

WKWebView, cannot take advantage of HTTPCookieStorage APIs to add the cookie.  Instead one has to use javascript evaluation to add the same cookie

let webConfiguration = WKWebViewConfiguration() 
myWKWebView = WKWebView(frame: .zero, configuration: webConfiguration)

let addCookieScript = "document.cookie = \'mycookie=abc;domain=mydomain;path=/\';"

...

// Call this only after document is loaded
myWKWebView.evaluateJavaScript(addCookieScript,completionHandler: { (result, error) in
            if completionHandler != nil {
               completionHandler(result, error)
            }
        })

Additionally, the above javascript evaluation will not be successful until a document is loaded!

Sharing Cookies

Quick Take: Use the same WKProcessPool to share cookies with other WKWebView

Since WKWebView is not able to take advantage of HTTPCookieStorage, you would need to create a common WKProcessPool to be used by WKWebViews that need to share cookies

let commonProcessPool : WKProcessPool = WKProcessPool()

let configuration1 = WKWebViewConfiguration()
configuration1.processPool = commonProcessPool
let webView1 :WKWebView = WKWebView(frame: CGRectZero, configuration: configuration1)

let configuration2 = WKWebViewConfiguration()
configuration2.processPool = commonProcessPool
let webView2 :WKWebView = WKWebView(frame: CGRectZero, configuration: configuration2)

Instruct WKWebView to add cookies once document is loaded

Quick Take: A javascript to add cookie can be set when the WKWebView is first initialized and can be instructed to run when the document is loaded

WKWebView can be instructed to add cookies once the document load and before any other scripts and logic are processed within the document.  This is especially helpful when some of the scripts are looking for certain cookies.  We can do this using the WKUserScript class and specify the injectionTime to .atDocumentStart (and if for some reason you need the script to be run at the end, then use .atDocumentEnd)

let addCookieScript="var cookieNames = document.cookie.split(\'; \').map(function(cookie) { return cookie.split(\'=\')[0] } );\nif (cookieNames.indexOf(\'mycookie\') == -1) { document.cookie=\'mycookie=abc;domain=mydomain;path=/\'; };\n"

let script = WKUserScript(source: addCookieScript, injectionTime: .atDocumentStart, forMainFrameOnly: false)

let config = WKWebViewConfiguration()
config.processPool = processPool

config.userContentController.addUserScript(script)

webView = WKWebView(frame: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(50), height: CGFloat(50)), configuration: wkconf)

What about time sensitive cookie?

Quick Take:  Use a message handler to add time sensitive cookie

The above example works well if the value of your cookie stays the same.  Eg if the same web view is used to load another document which also needs the same cookies, then your work is done.  However, what if the value of the cookies changes eg if they are time sensitive, then using the above example could result in stale data.  Fortunately we can modify the example to callback to use a message handler to set the cookie with the latest value when the document loads

class MyWKWebViewController: NSObject, WKNavigationDelegate,  WKScriptMessageHandler {

override init() {
  // this script callback when the document load
  let script = WKUserScript(source: "window.webkit.messageHandlers.MyListener.postMessage(\"setCookiesForInitialLoad\");", injectionTime: .atDocumentStart, forMainFrameOnly: false)

  let config = WKWebViewConfiguration()
  config.processPool = processPool

// Note make sure the name of the message handler, in this case 'MyListener' is the same the one specified in the script
  config.userContentController.addUserScript(script)      conf.userContentController.add(self, name: "MyListener")

  webView = WKWebView(frame: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(50), height: CGFloat(50)), configuration: config)

}

// MARK: Message Handler methods
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

  let messageValue = message.body as! String;
  switch (messageValue) {
     case "setCookiesForInitialLoad":
          self.webView.evaluateJavaScript(self.addTimeSensitiveCookieScript, completionHandler: nil)
                break;
            default:
                break
   }
}

func addTimeSensitiveCookieScript() -> String! {
   let newValue : String = self.getSomeTimeSensitiveValue()
   let addCookieScript="var cookieNames = document.cookie.split(\'; \').map(function(cookie) { return cookie.split(\'=\')[0] } );\nif (cookieNames.indexOf(\'mycookie\') == -1) { document.cookie=\'mycookie=\(newValue);domain=mydomain;path=/\'; };\n"

    return addCookieScript
 }

Deleting Cookies

Cookies are not automatically deleted when the WKWebView is deleted, have to manually delete them

As with UIWebView, cookies are not deleted when they are deleted, in the case of WKWebView, once again, you are not able to delete cookies using HTTPCookieStorage APIs.  You can use the following (which will not only delete cookies but any cache data as well.  Note some cookies are needed for signing out etc, so do not delete cookies until they are used for the necessary clean up.

func deleteWebViewData(completion: @escaping () -> Void) {
  let dateFrom = NSDate(timeIntervalSince1970: 0)
  let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes()

  WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom as Date, completionHandler: {() -> Void in
  UserDefaults.standard.synchronize()
  completion()
  })
}

That’s all for now, hopefully its enough to chew for the time being.

I would like to leave you with a link to a slide show that Mobify put together about their experience with WKWebView: