iOS
cookie
Swift
WKWebView

iOS9, 10 WKWebView - Cookie操作

ブラウザ機能を持つアプリの開発で、UIWebViewからWKWebViewへ移行した際にCookie操作で詰まったので手順をまとめておきます。
様々な方法を試した結果、最終的に下記方法に落ち着きました。

WKWebView間のCookie同期

複数のWKWebViewを使う場合は、WKProcessPoolを共有することでリアルタイムでCookieの同期が可能。

ViewController.swift
import WebKit

final class ViewController: UIViewController {

    private lazy var sharedProcessPool: WKProcessPool = WKProcessPool()
    private var webView: WKWebView?

    func setUpWKWebView() {
        let configuration = WKWebViewConfiguration()

        // 共有しているprocessPoolを使う
        configuration.processPool = sharedProcessPool

        let webView = WKWebView(frame: .zero, configuration: configuration)

        webView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(webView)

        NSLayoutConstraint.activate(
            [
                webView.topAnchor.constraint(equalTo: view.topAnchor),
                webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ]
        )

        self.webView = webView
    }
}

[iOS9, 10] Cookieの参照

iOS9, 10では直接参照する方法がないので、WKUserScriptを使いJavaScript経由で取得する

let getCookiesStringHandler = "getCookiesStringHandler"
fileprivate var cookiesStringCompletion: ((_ cookiesString: String?) -> Void)?

// JavaScriptで document.cookie 結果を取得する
func getCookiesString(completion: @escaping (_ cookiesString: String?) -> Void) {
    self.cookiesStringCompletion = completion

    let htmlTemplate = "<DOCTYPE html><html><body></body></html>"
    let dummyBaseURL = URL(string: "https://dummy.hoge.fuga")
    let javaScriptString = "webkit.messageHandlers.\(self.getCookiesStringHandler).postMessage(document.cookie)"

    let userScript = WKUserScript(
        source: javaScriptString,
        injectionTime: .atDocumentEnd,
        forMainFrameOnly: true
    )

    let controller = WKUserContentController()
    controller.addUserScript(userScript)
    controller.add(self, name: self.getCookiesStringHandler)

    let configuration = WKWebViewConfiguration()
    configuration.userContentController = controller
    configuration.processPool = self.sharedProcessPool

    let webView = WKWebView(frame: .zero, configuration: configuration)
    webView.loadHTMLString(htmlTemplate, baseURL: dummyBaseURL)

    self.webView = webView
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {    
    self.webView?.stopLoading()
    self.webView = nil

    if message.name == self.getCookiesStringHandler {
        if let cookiesString = message.body as? String {
            self.cookiesStringCompletion?(cookiesString)
        } else {
            self.cookiesStringCompletion?(nil)
        }
    }
}

[iOS9, 10] Cookieの更新

基本cookieを更新したい場合は、サーバーサイドで行えば良いので発行・更新するエントリポイントにWKWebViewでloadRequestすれば良い。
ローカルで更新が必要な場合は、参照時と同様にWKUserScriptを使ってJavaScript経由で更新する

let setCookieHandler = "setCookieHandler"
fileprivate var setCookieCompletion: (() -> Void)?

// JavaScriptで document.cookie を使って更新する
func setCookie(name: String, value: String, completion: @escaping () -> Void) {
    self.setCookieCompletion = completion

    let htmlTemplate = "<DOCTYPE html><html><body></body></html>"
    let dummyBaseURL = URL(string: "https://dummy.hoge.fuga")
    let javaScriptString = "webkit.messageHandlers.\(self.setCookieHandler).postMessage(document.cookie='\(name)=\(value)')"

    let userScript = WKUserScript(
        source: javaScriptString,
        injectionTime: .atDocumentEnd,
        forMainFrameOnly: true
    )

    let controller = WKUserContentController()
    controller.addUserScript(userScript)
    controller.add(self, name: self.setCookieHandler)

    let configuration = WKWebViewConfiguration()
    configuration.userContentController = controller
    configuration.processPool = self.sharedProcessPool

    let webView = WKWebView(frame: .zero, configuration: configuration)

    webView.loadHTMLString(htmlTemplate, baseURL: dummyBaseURL)

    self.webView = webView
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    self.webView?.stopLoading()
    self.webView = nil

    if message.name == self.setCookieHandler {
        self.setCookieCompletion?()
    }
}

[iOS9, 10] Cookieの監視

WKWebViewのcookieはWKWebsiteDataStoreで管理されており、iOS9, 10では更新があってもNSHTTPCookieManagerCookiesChanged は通知されない
自前でタイマー使うか適切なタイミングで参照して変更を検知する

サンプルコード

https://github.com/ysakui/WKWebViewCookieSync

まとめ

  • 共有のWKProcessPoolを使う
  • ダミーURLを使い、ローカルHTMLを読み込む
    • 通信は発生しないため、オフラインでの参照・更新が可能
  • addSubView不要
  • 参照時はJavaScriptでnameやexpireを指定か、document.cookieで取得した結果をパースして使う

他にも良い方法あればぜひ教えてください。
iOS11の実装はまた追記します😓