Use case: I have two WKWebView
s, one in a main app and one in an app extension. I want them to share the same cookie store.
Apple APIs have 2 different cookie storage classes: WKHTTPCookieStore
and HTTPCookieStorage
.
Apple also has sharedCookieStorage(forGroupContainerIdentifier identifier: String) -> HTTPCookieStorage
for URLSession
s (not WKWebView
s) to use across Apps/Build Targets/Extensions.
My plan is to transfer one WKWebKit
cookies to the sharedCookieStorage and then again to the other WKWebKit
cookie store.
Before I write one, does anyone have a simple wrapper class that takes in these classes, observes them, and keeps them synchronized?
Or is there otherwise a simpler way to do this seemingly very common use case?
This is what I put together. Important things to keep in mind:
WKWebView
cookies are set and fetched asynchronously, hence the number of callbacksWKHTTPCookieStore
. It's not perfect, but cookiesDidChange
methods are there for manually updating, say after page loadsWKWebView
within the addCookieStore
callback to ensure the store is sync'd before the page is loadedaddCookieStore
callbacks to ensure the proper order(Sorry I couldn't use Combine for compatibility reasons)
Code:
import WebKit
import Foundation
class CookieSync : NSObject, WKHTTPCookieStoreObserver {
var wkStores = [WKHTTPCookieStore]();
var sessionStores = [HTTPCookieStorage]();
static let debug = false
//The first store added is the canon
func addCookieStore(_ store: WKHTTPCookieStore, callback:(()->())?) {
wkStores.append(store)
store.getAllCookies { (cookies) in
if CookieSync.debug { print("Adding WK:\(cookies.count)") }
store.add(self)
if self.sessionStores.count > 0 {
self.synchronizeAll(self.sessionStores[0]) {
store.getAllCookies { (cookies) in
if CookieSync.debug { print("Added WK:\(cookies.count)") }
callback?()
}
}
} else if self.wkStores.count > 1 {
self.synchronizeAll(self.wkStores[0]) {
store.getAllCookies { (cookies) in
if CookieSync.debug { print("Added WK:\(cookies.count)") }
callback?()
}
}
} else {
callback?()
}
}
}
//The first store added is the canon
func addCookieStore(_ store: HTTPCookieStorage, callback:(()->())?) {
sessionStores.append(store)
if CookieSync.debug { print("Adding S:\(store.cookies?.count ?? 0)") }
if wkStores.count > 0 {
synchronizeAll(wkStores[0]) {
if CookieSync.debug { print("Added S:\(store.cookies?.count ?? 0)") }
callback?()
}
} else if sessionStores.count > 1 {
synchronizeAll(sessionStores[0]) {
if CookieSync.debug { print("Added S:\(store.cookies?.count ?? 0)") }
callback?()
}
} else {
callback?()
}
}
//There is no Observer callback for HTTPCookieStorage
func cookiesDidChange(in cookieStore: HTTPCookieStorage) {
synchronizeAll(cookieStore) {
if CookieSync.debug { print("Synced S:\(cookieStore.cookies?.count ?? 0)") }
}
}
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
synchronizeAll(cookieStore) {
cookieStore.getAllCookies { (cookies) in
if CookieSync.debug { print("Synced WK:\(cookies.count)") }
for cookie in cookies {
if CookieSync.debug { print("\(cookie.name) = \(cookie.value)") }
}
}
}
}
//Private
fileprivate func synchronizeAll(_ to: WKHTTPCookieStore, callback:(()->())?) {
let dispatch = DispatchGroup()
let queue = Thread.isMainThread ? DispatchQueue.main : DispatchQueue(label: "cookie_sync1")
for store in self.wkStores {
if store == to { continue }
dispatch.enter()
self.removeAllCookies(store) {
dispatch.enter()
to.getAllCookies { (cookies) in
for cookie in cookies {
dispatch.enter()
store.setCookie(cookie) {
dispatch.leave()
}
}
dispatch.leave()
}
dispatch.leave()
}
}
for store in self.sessionStores {
self.removeAllCookies(store)
dispatch.enter()
to.getAllCookies { (cookies) in
for cookie in cookies {
store.setCookie(cookie)
}
dispatch.leave()
}
}
dispatch.notify(queue: queue) {
callback?()
}
}
fileprivate func synchronizeAll(_ to: HTTPCookieStorage, callback:(()->())?) {
guard let cookies = to.cookies else { callback?(); return; }
let queue = Thread.isMainThread ? DispatchQueue.main : DispatchQueue(label: "cookie_sync2")
let dispatch = DispatchGroup()
for store in self.sessionStores {
if store == to { continue }
self.removeAllCookies(store)
for cookie in cookies {
store.setCookie(cookie)
}
}
for store in self.wkStores {
dispatch.enter()
self.removeAllCookies(store) {
for cookie in cookies {
dispatch.enter()
store.setCookie(cookie) {
dispatch.leave()
}
}
dispatch.leave()
}
}
dispatch.notify(queue: queue) {
callback?()
}
}
fileprivate func removeAllCookies(_ store: WKHTTPCookieStore, callback:(()->())?) {
let queue = Thread.isMainThread ? DispatchQueue.main : DispatchQueue(label: "cookie_delete")
let dispatch = DispatchGroup()
dispatch.enter()
store.getAllCookies { (cookies) in
for cookie in cookies {
dispatch.enter()
store.delete(cookie) {
dispatch.leave()
}
}
dispatch.leave()
}
dispatch.notify(queue: queue) {
callback?()
}
}
fileprivate func removeAllCookies(_ store: HTTPCookieStorage) {
guard let cookies = store.cookies else { return }
for cookie in cookies {
store.deleteCookie(cookie)
}
}
}