iOS9, 10 WKWebView - Cookie操作

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

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

```swift: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経由で取得する

```swift
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経由で更新する

```swift
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の実装はまた追記します😓