150
votes

I'm trying to switch an existing app from UIWebView to WKWebView. The current app manages the users login / session outside of the webview and sets the cookies required for authentication into the the NSHTTPCookieStore. Unfortunately new WKWebView doesn't use the cookies from the NSHTTPCookieStorage. Is there another way to achieve this?

24

24 Answers

212
votes

Edit for iOS 11+ only

Use WKHTTPCookieStore:

let cookie = HTTPCookie(properties: [
    .domain: "example.com",
    .path: "/",
    .name: "MyCookieName",
    .value: "MyCookieValue",
    .secure: "TRUE",
    .expires: NSDate(timeIntervalSinceNow: 31556926)
])! 

webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)

Since you are pulling them over from HTTPCookeStorage, you can do this:

let cookies = HTTPCookieStorage.shared.cookies ?? []
for cookie in cookies {
    webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}

Old answer for iOS 10 and below

If you require your cookies to be set on the initial load request, you can set them on NSMutableURLRequest. Because cookies are just a specially formatted request header this can be achieved like so:

WKWebView * webView = /*set up your webView*/
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com/index.html"]];
[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"];
// use stringWithFormat: in the above line to inject your values programmatically
[webView loadRequest:request];

If you require subsequent AJAX requests on the page to have their cookies set, this can be achieved by simply using WKUserScript to set the values programmatically via javascript at document start like so:

WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] 
    initWithSource: @"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
    injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
// again, use stringWithFormat: in the above line to inject your values programmatically
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];

Combining these two techniques should give you enough tools to transfer cookie values from Native App Land to Web View Land. You can find more info on the cookie javascript API on Mozilla's page if you require some more advanced cookies.

Yeah, it sucks that Apple is not supporting many of the niceties of UIWebView. Not sure if they will ever support them, but hopefully they will get on this soon. Hope this helps!

68
votes

After playing with this answer (which was fantastically helpful :) we've had to make a few changes:

  • We need web views to deal with multiple domains without leaking private cookie information between those domains
  • We need it to honour secure cookies
  • If the server changes a cookie value we want our app to know about it in NSHTTPCookieStorage
  • If the server changes a cookie value we don't want our scripts to reset it back to its original value when you follow a link / AJAX etc.

So we modified our code to be this;

Creating a request

NSMutableURLRequest *request = [originalRequest mutableCopy];
NSString *validDomain = request.URL.host;
const BOOL requestIsSecure = [request.URL.scheme isEqualToString:@"https"];

NSMutableArray *array = [NSMutableArray array];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
    // Don't even bother with values containing a `'`
    if ([cookie.name rangeOfString:@"'"].location != NSNotFound) {
        NSLog(@"Skipping %@ because it contains a '", cookie.properties);
        continue;
    }

    // Is the cookie for current domain?
    if (![cookie.domain hasSuffix:validDomain]) {
        NSLog(@"Skipping %@ (because not %@)", cookie.properties, validDomain);
        continue;
    }

    // Are we secure only?
    if (cookie.secure && !requestIsSecure) {
        NSLog(@"Skipping %@ (because %@ not secure)", cookie.properties, request.URL.absoluteString);
        continue;
    }

    NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
    [array addObject:value];
}

NSString *header = [array componentsJoinedByString:@";"];
[request setValue:header forHTTPHeaderField:@"Cookie"];

// Now perform the request...

This makes sure that the first request has the correct cookies set, without sending any cookies from the shared storage that are for other domains, and without sending any secure cookies into an insecure request.

Dealing with further requests

We also need to make sure that other requests have the cookies set. This is done using a script that runs on document load which checks to see if there is a cookie set and if not, set it to the value in NSHTTPCookieStorage.

// Get the currently set cookie names in javascriptland
[script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];

for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
    // Skip cookies that will break our script
    if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
        continue;
    }

    // Create a line that appends this cookie to the web view's document's cookies
    [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.wn_javascriptString];
}

WKUserContentController *userContentController = [[WKUserContentController alloc] init];
WKUserScript *cookieInScript = [[WKUserScript alloc] initWithSource:script
                                                      injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                   forMainFrameOnly:NO];
[userContentController addUserScript:cookieInScript];

...

// Create a config out of that userContentController and specify it when we create our web view.
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = userContentController;

self.webView = [[WKWebView alloc] initWithFrame:webView.bounds configuration:config];

Dealing with cookie changes

We also need to deal with the server changing a cookie's value. This means adding another script to call back out of the web view we are creating to update our NSHTTPCookieStorage.

WKUserScript *cookieOutScript = [[WKUserScript alloc] initWithSource:@"window.webkit.messageHandlers.updateCookies.postMessage(document.cookie);"
                                                       injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                    forMainFrameOnly:NO];
[userContentController addUserScript:cookieOutScript];

[userContentController addScriptMessageHandler:webView
                                          name:@"updateCookies"];

and implementing the delegate method to update any cookies that have changed, making sure that we are only updating cookies from the current domain!

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSArray<NSString *> *cookies = [message.body componentsSeparatedByString:@"; "];
    for (NSString *cookie in cookies) {
        // Get this cookie's name and value
        NSArray<NSString *> *comps = [cookie componentsSeparatedByString:@"="];
        if (comps.count < 2) {
            continue;
        }

        // Get the cookie in shared storage with that name
        NSHTTPCookie *localCookie = nil;
        for (NSHTTPCookie *c in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:self.wk_webView.URL]) {
            if ([c.name isEqualToString:comps[0]]) {
                localCookie = c;
                break;
            }
        }

        // If there is a cookie with a stale value, update it now.
        if (localCookie) {
            NSMutableDictionary *props = [localCookie.properties mutableCopy];
            props[NSHTTPCookieValue] = comps[1];
            NSHTTPCookie *updatedCookie = [NSHTTPCookie cookieWithProperties:props];
            [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:updatedCookie];
        }
    }
}

This seems to fix our cookie problems without us having to deal with each place we use WKWebView differently. We can now just use this code as a helper to create our web views and it transparently updates NSHTTPCookieStorage for us.


EDIT: Turns out I used a private category on NSHTTPCookie - here's the code:

- (NSString *)wn_javascriptString {
    NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
                        self.name,
                        self.value,
                        self.domain,
                        self.path ?: @"/"];

    if (self.secure) {
        string = [string stringByAppendingString:@";secure=true"];
    }

    return string;
}
58
votes

The cookies must be set on the configuration before the WKWebView is created. Otherwise, even with WKHTTPCookieStore's setCookie completion handler, the cookies won't reliably be synced to the web view. This goes back to this line from the docs on WKWebViewConfiguration

@NSCopying var configuration: WKWebViewConfiguration { get }

That @NSCopying is somewhat of a deep copy. The implementation is beyond me, but the end result is that unless you set cookies before initializing the webview, you can't count on the cookies being there. This can complicate app architecture because initializing a view becomes an asynchronous process. You'll end up with something like this

extension WKWebViewConfiguration {
    /// Async Factory method to acquire WKWebViewConfigurations packaged with system cookies
    static func cookiesIncluded(completion: @escaping (WKWebViewConfiguration?) -> Void) {
        let config = WKWebViewConfiguration()
        guard let cookies = HTTPCookieStorage.shared.cookies else {
            completion(config)
            return
        }
        // Use nonPersistent() or default() depending on if you want cookies persisted to disk
        // and shared between WKWebViews of the same app (default), or not persisted and not shared
        // across WKWebViews in the same app.
        let dataStore = WKWebsiteDataStore.nonPersistent()
        let waitGroup = DispatchGroup()
        for cookie in cookies {
            waitGroup.enter()
            dataStore.httpCookieStore.setCookie(cookie) { waitGroup.leave() }
        }
        waitGroup.notify(queue: DispatchQueue.main) {
            config.websiteDataStore = dataStore
            completion(config)
        }
    }
}

and then to use it something like

override func loadView() {
    view = UIView()
    WKWebViewConfiguration.cookiesIncluded { [weak self] config in
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.load(request)
        self.view = webView
    }
}

The above example defers view creation until the last possible moment, another solution would be to create the config or webview well in advance and handle the asynchronous nature before creation of a view controller.

A final note: once you create this webview, you have set it loose into the wild, you can't add more cookies without using methods described in this answer. You can however use the WKHTTPCookieStoreObserver api to at least observe changes happening to cookies. So if a session cookie gets updated in the webview, you can manually update the system's HTTPCookieStorage with this new cookie if desired.

For more on this, skip to 18:00 at this 2017 WWDC Session Custom Web Content Loading. At the beginning of this session, there is a deceptive code sample which omits the fact that the webview should be created in the completion handler.

cookieStore.setCookie(cookie!) {
    webView.load(loggedInURLRequest)
}

The live demo at 18:00 clarifies this.

Edit As of Mojave Beta 7 and iOS 12 Beta 7 at least, I'm seeing much more consistent behavior with cookies. The setCookie(_:) method even appears to allow setting cookies after the WKWebView has been created. I did find it important though, to not touch the processPool variable at all. The cookie setting functionality works best when no additional pools are created and when that property is left well alone. I think it's safe to say we were having issues due to some bugs in WebKit.

25
votes

work for me

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
    let headerFields = navigationAction.request.allHTTPHeaderFields
    var headerIsPresent = contains(headerFields?.keys.array as! [String], "Cookie")

    if headerIsPresent {
        decisionHandler(WKNavigationActionPolicy.Allow)
    } else {
        let req = NSMutableURLRequest(URL: navigationAction.request.URL!)
        let cookies = yourCookieData
        let values = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies)
        req.allHTTPHeaderFields = values
        webView.loadRequest(req)

        decisionHandler(WKNavigationActionPolicy.Cancel)
    }
}
19
votes

Here is my version of Mattrs solution in Swift for injecting all cookies from HTTPCookieStorage. This was done mainly to inject an authentication cookie to create a user session.

public func setupWebView() {
    let userContentController = WKUserContentController()
    if let cookies = HTTPCookieStorage.shared.cookies {
        let script = getJSCookiesString(for: cookies)
        let cookieScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)
        userContentController.addUserScript(cookieScript)
    }
    let webViewConfig = WKWebViewConfiguration()
    webViewConfig.userContentController = userContentController

    self.webView = WKWebView(frame: self.webViewContainer.bounds, configuration: webViewConfig)
}

///Generates script to create given cookies
public func getJSCookiesString(for cookies: [HTTPCookie]) -> String {
    var result = ""
    let dateFormatter = DateFormatter()
    dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
    dateFormatter.dateFormat = "EEE, d MMM yyyy HH:mm:ss zzz"

    for cookie in cookies {
        result += "document.cookie='\(cookie.name)=\(cookie.value); domain=\(cookie.domain); path=\(cookie.path); "
        if let date = cookie.expiresDate {
            result += "expires=\(dateFormatter.stringFromDate(date)); "
        }
        if (cookie.secure) {
            result += "secure; "
        }
        result += "'; "
    }
    return result
}
10
votes

set cookie

self.webView.evaluateJavaScript("document.cookie='access_token=your token';domain='your domain';") { (data, error) -> Void in
        self.webView.reload()
}

delete cookie

self.webView.evaluateJavaScript("document.cookie='access_token=';domain='your domain';") { (data, error) -> Void in
        self.webView.reload()
}
9
votes

Swift 3 update :

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
    if let urlResponse = navigationResponse.response as? HTTPURLResponse,
       let url = urlResponse.url,
       let allHeaderFields = urlResponse.allHeaderFields as? [String : String] {
       let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: url)
       HTTPCookieStorage.shared.setCookies(cookies , for: urlResponse.url!, mainDocumentURL: nil)
       decisionHandler(.allow)
    }
}
8
votes

After looking through various answers here and not having any success, I combed through the WebKit documentation and stumbled upon the requestHeaderFields static method on HTTPCookie, which converts an array of cookies into a format suitable for a header field. Combining this with mattr's insight of updating the URLRequest before loading it with the cookie headers got me through the finish line.

Swift 4.1, 4.2, 5.0:

var request = URLRequest(url: URL(string: "https://example.com/")!)
let headers = HTTPCookie.requestHeaderFields(with: cookies)
for (name, value) in headers {
    request.addValue(value, forHTTPHeaderField: name)
}

let webView = WKWebView(frame: self.view.frame)
webView.load(request)

To make this even simpler, use an extension:

extension WKWebView {
    func load(_ request: URLRequest, with cookies: [HTTPCookie]) {
        var request = request
        let headers = HTTPCookie.requestHeaderFields(with: cookies)
        for (name, value) in headers {
            request.addValue(value, forHTTPHeaderField: name)
        }

        load(request)
    }
}

Now it just becomes:

let request = URLRequest(url: URL(string: "https://example.com/")!)
let webView = WKWebView(frame: self.view.frame)
webView.load(request, with: cookies)

This extension is also available in LionheartExtensions if you just want a drop-in solution. Cheers!

7
votes

In iOS 11, you can manage cookie now :), see this session: https://developer.apple.com/videos/play/wwdc2017/220/

enter image description here

6
votes

The reason behind posted this answer is I tried many solution but no one work properly, most of the answer not work in case where have to set cookie first time, and got result cookie not sync first time, Please use this solution it work for both iOS >= 11.0 <= iOS 11 till 8.0, also work with cookie sync first time.

For iOS >= 11.0 -- Swift 4.2

Get http cookies and set in wkwebview cookie store like this way, it's very tricky point to load your request in wkwebview, must sent request for loading when cookies gonna be set completely, here is function that i wrote.

Call function with closure in completion you call load webview. FYI this function only handle iOS >= 11.0

self.WwebView.syncCookies {
    if let request = self.request {
       self.WwebView.load(request)
    }
}

Here is implementation for syncCookies function.

func syncCookies(completion:@escaping ()->Void) {

if #available(iOS 11.0, *) {

      if let yourCookie = "HERE_YOUR_HTTP_COOKIE_OBJECT" {
        self.configuration.websiteDataStore.httpCookieStore.setCookie(yourCookie, completionHandler: {
              completion()
        })
     }
  } else {
  //Falback just sent 
  completion()
}
}

For iOS 8 till iOS 11

you need to setup some extra things you need to set two time cookies one through using WKUserScript and dont forget to add cookies in request as well, otherwise your cookie not sync first time and you will see you page not load first time properly. this is the heck that i found to support cookies for iOS 8.0

before you Wkwebview object creation.

func setUpWebView() {

    let userController: WKUserContentController = WKUserContentController.init()

    if IOSVersion.SYSTEM_VERSION_LESS_THAN(version: "11.0") {
        if let cookies = HTTPCookieStorage.shared.cookies {
            if let script = getJSCookiesString(for: cookies) {
                cookieScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)
                userController.addUserScript(cookieScript!)
            }
        }
    }

    let webConfiguration = WKWebViewConfiguration()
    webConfiguration.processPool = BaseWebViewController.processPool


    webConfiguration.userContentController = userController


    let customFrame = CGRect.init(origin: CGPoint.zero, size: CGSize.init(width: 0.0, height: self.webContainerView.frame.size.height))
    self.WwebView = WKWebView (frame: customFrame, configuration: webConfiguration)
    self.WwebView.translatesAutoresizingMaskIntoConstraints = false
    self.webContainerView.addSubview(self.WwebView)
    self.WwebView.uiDelegate = self
    self.WwebView.navigationDelegate = self
    self.WwebView.allowsBackForwardNavigationGestures = true // A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations
    self.WwebView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)


 self.view.addConstraint(NSLayoutConstraint(item: WwebView, attribute: .trailing, relatedBy: .equal, toItem: self.webContainerView, attribute: .trailing, multiplier: 1, constant: 0))
    self.view.addConstraint(NSLayoutConstraint(item: WwebView, attribute: .leading, relatedBy: .equal, toItem: self.webContainerView, attribute: .leading, multiplier: 1, constant: 0))
    self.view.addConstraint(NSLayoutConstraint(item: WwebView, attribute: .top, relatedBy: .equal, toItem: self.webContainerView, attribute: .top, multiplier: 1, constant: 0))
    self.view.addConstraint(NSLayoutConstraint(item: WwebView, attribute: .bottom, relatedBy: .equal, toItem: self.webContainerView, attribute: .bottom, multiplier: 1, constant: 0))


}

Focus on this function getJSCookiesString

 public func getJSCookiesString(for cookies: [HTTPCookie]) -> String? {

    var result = ""
    let dateFormatter = DateFormatter()
    dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
    dateFormatter.dateFormat = "EEE, d MMM yyyy HH:mm:ss zzz"

    for cookie in cookies {
        if cookie.name == "yout_cookie_name_want_to_sync" {
            result += "document.cookie='\(cookie.name)=\(cookie.value); domain=\(cookie.domain); path=\(cookie.path); "
            if let date = cookie.expiresDate {
                result += "expires=\(dateFormatter.string(from: date)); "
            }
            if (cookie.isSecure) {
                result += "secure; "
            }
            result += "'; "
        }

    }

    return result
}

Here is other step wkuserscript not sync cookies immediately, there a lot of heck to load first time page with cookie one is to reload webview again if it terminate process but i don't recommend to use it, its not good for user point of view, heck is whenever you ready to load request set cookies in request header as well like this way, don't forget to add iOS version check. before load request call this function.

request?.addCookies()

i wrote extension for URLRequest

extension URLRequest {

internal mutating func addCookies() {
    //"appCode=anAuY28ucmFrdXRlbi5yZXdhcmQuaW9zLXpOQlRTRmNiejNHSzR0S0xuMGFRb0NjbUg4Ql9JVWJH;rpga=kW69IPVSYZTo0JkZBicUnFxC1g5FtoHwdln59Z5RNXgJoMToSBW4xAMqtf0YDfto;rewardadid=D9F8CE68-CF18-4EE6-A076-CC951A4301F6;rewardheader=true"
    var cookiesStr: String = ""

    if IOSVersion.SYSTEM_VERSION_LESS_THAN(version: "11.0") {
        let mutableRequest = ((self as NSURLRequest).mutableCopy() as? NSMutableURLRequest)!
        if let yourCookie = "YOUR_HTTP_COOKIE_OBJECT" {
            // if have more than one cookies dont forget to add ";" at end
            cookiesStr += yourCookie.name + "=" + yourCookie.value + ";"

            mutableRequest.setValue(cookiesStr, forHTTPHeaderField: "Cookie")
            self = mutableRequest as URLRequest

        }
    }

  }
}

now you ready to go for testing iOS > 8

2
votes

Please find the solution which most likely will work for you out of the box. Basically it's modified and updated for Swift 4 @user3589213's answer.

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    let headerKeys = navigationAction.request.allHTTPHeaderFields?.keys
    let hasCookies = headerKeys?.contains("Cookie") ?? false

    if hasCookies {
        decisionHandler(.allow)
    } else {
        let cookies = HTTPCookie.requestHeaderFields(with: HTTPCookieStorage.shared.cookies ?? [])

        var headers = navigationAction.request.allHTTPHeaderFields ?? [:]
        headers += cookies

        var req = navigationAction.request
        req.allHTTPHeaderFields = headers

        webView.load(req)

        decisionHandler(.cancel)
    }
}
2
votes

I have tried all of the answers above but none of them work. After so many attempts I've finally found a reliable way to set WKWebview cookie.

First you have to create an instance of WKProcessPool and set it to the WKWebViewConfiguration that is to be used to initialize the WkWebview itself:

    private lazy var mainWebView: WKWebView = {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.processPool = WKProcessPool()
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = self
        return webView
    }()

Setting WKProcessPool is the most important step here. WKWebview makes use of process isolation - which means it runs on a different process than the process of your app. This can sometimes cause conflict and prevent your cookie from being synced properly with the WKWebview.

Now let's look at the definition of WKProcessPool

The process pool associated with a web view is specified by its web view configuration. Each web view is given its own Web Content process until an implementation-defined process limit is reached; after that, web views with the same process pool end up sharing Web Content processes.

Pay attention to the last sentence if you plan to use the same WKWebview for subsequence requests

web views with the same process pool end up sharing Web Content processes

what I means is that if you don't use the same instance of WKProcessPool each time you configure a WKWebView for the same domain (maybe you have a VC A that contains a WKWebView and you want to create different instances of VC A in different places), there can be conflict setting cookies. To solve the problem, after the first creation of the WKProcessPool for a WKWebView that loads domain B, I save it in a singleton and use that same WKProcessPool every time I have to create a WKWebView that loads the same domain B

private lazy var mainWebView: WKWebView = {
    let webConfiguration = WKWebViewConfiguration()
    if Enviroment.shared.processPool == nil {
        Enviroment.shared.processPool = WKProcessPool()
    }
    webConfiguration.processPool = Enviroment.shared.processPool!
    webConfiguration.processPool = WKProcessPool()
    let webView = WKWebView(frame: .zero, configuration: webConfiguration)
    webView.navigationDelegate = self
    return webView
}()

After the initialization process, you can load an URLRequest inside the completion block of httpCookieStore.setCookie. Here, you have to attach the cookie to the request header otherwise it won't work.

P/s: I stole the extension from the fantastic answer above by Dan Loewenherz

mainWebView.configuration.websiteDataStore.httpCookieStore.setCookie(your_cookie) {
        self.mainWebView.load(your_request, with: [your_cookie])
}

extension WKWebView {
   func load(_ request: URLRequest, with cookies: [HTTPCookie]) {
      var request = request
      let headers = HTTPCookie.requestHeaderFields(with: cookies)
      for (name, value) in headers {
         request.addValue(value, forHTTPHeaderField: name)
      }        
      load(request)
   }
}
2
votes

My version of nteiss's answer. Tested on iOS 11, 12, 13. Looks like you don't have to use DispatchGroup on iOS 13 anymore.

I use non-static function includeCustomCookies on WKWebViewConfiguration, so that I can update cookies every time I create new WKWebViewConfiguration.

extension WKWebViewConfiguration {
    func includeCustomCookies(cookies: [HTTPCookie], completion: @escaping  () -> Void) {
        let dataStore = WKWebsiteDataStore.nonPersistent()
        let waitGroup = DispatchGroup()

        for cookie in cookies {
            waitGroup.enter()
            dataStore.httpCookieStore.setCookie(cookie) { waitGroup.leave() }
        }

        waitGroup.notify(queue: DispatchQueue.main) {
            self.websiteDataStore = dataStore
            completion()
        }
    }
}

Then I use it like this:

let customUserAgent: String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15"

let customCookies: [HTTPCookie] = {
    let cookie1 = HTTPCookie(properties: [
        .domain: "yourdomain.com",
        .path: "/",
        .name: "auth_token",
        .value: APIManager.authToken
    ])!

    let cookie2 = HTTPCookie(properties: [
        .domain: "yourdomain.com",
        .path: "/",
        .name: "i18next",
        .value: "ru"
    ])!

    return [cookie1, cookie2]
}()

override func viewDidLoad() {
    super.viewDidLoad()

    activityIndicatorView.startAnimating()

    let webConfiguration = WKWebViewConfiguration()
    webConfiguration.includeCustomCookies(cookies: customCookies, completion: { [weak self] in
        guard let strongSelf = self else { return }
        strongSelf.webView = WKWebView(frame: strongSelf.view.bounds, configuration: webConfiguration)
        strongSelf.webView.customUserAgent = strongSelf.customUserAgent
        strongSelf.webView.navigationDelegate = strongSelf
        strongSelf.webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        strongSelf.view.addSubview(strongSelf.webView)
        strongSelf.view.bringSubviewToFront(strongSelf.activityIndicatorView)
        strongSelf.webView.load(strongSelf.request)
    })
}
1
votes

Solution for iOS 10+

Details

  • Swift 5.1
  • Xcode 11.6 (11E708)

Solution

import UIKit
import WebKit
extension WKWebViewConfiguration {
    func set(cookies: [HTTPCookie], completion: (() -> Void)?) {
        if #available(iOS 11.0, *) {
            let waitGroup = DispatchGroup()
            for cookie in cookies {
                waitGroup.enter()
                websiteDataStore.httpCookieStore.setCookie(cookie) { waitGroup.leave() }
            }
            waitGroup.notify(queue: DispatchQueue.main) { completion?() }
        } else {
            cookies.forEach { HTTPCookieStorage.shared.setCookie($0) }
            self.createCookiesInjectionJS(cookies: cookies) {
                let script = WKUserScript(source: $0, injectionTime: .atDocumentStart, forMainFrameOnly: false)
                self.userContentController.addUserScript(script)
                DispatchQueue.main.async { completion?() }
            }
        }
    }

    private func createCookiesInjectionJS (cookies: [HTTPCookie],  completion: ((String) -> Void)?) {
        var scripts: [String] = ["var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } )"]
        let now = Date()

        for cookie in cookies {
            if let expiresDate = cookie.expiresDate, now.compare(expiresDate) == .orderedDescending { continue }
            scripts.append("if (cookieNames.indexOf('\(cookie.name)') == -1) { document.cookie='\(cookie.javaScriptString)'; }")
        }
        completion?(scripts.joined(separator: ";\n"))
    }
}

extension WKWebView {
    func loadWithCookies(request: URLRequest) {
        if #available(iOS 11.0, *) {
            load(request)
        } else {
            var _request = request
            _request.setCookies()
            load(_request)
        }
    }
}

extension URLRequest {

    private static var cookieHeaderKey: String { "Cookie" }
    private static var noAppliedcookieHeaderKey: String { "No-Applied-Cookies" }

    var hasCookies: Bool {
        let headerKeys = (allHTTPHeaderFields ?? [:]).keys
        var hasCookies = false
        if headerKeys.contains(URLRequest.cookieHeaderKey) { hasCookies = true }
        if !hasCookies && headerKeys.contains(URLRequest.noAppliedcookieHeaderKey) { hasCookies = true }
        return hasCookies
    }

    mutating func setCookies() {
        if #available(iOS 11.0, *) { return }
        var cookiesApplied = false
        if let url = self.url, let cookies = HTTPCookieStorage.shared.cookies(for: url) {
            let headers = HTTPCookie.requestHeaderFields(with: cookies)
            for (name, value) in headers { setValue(value, forHTTPHeaderField: name) }
            cookiesApplied = allHTTPHeaderFields?.keys.contains(URLRequest.cookieHeaderKey) ?? false
        }
        if !cookiesApplied { setValue("true", forHTTPHeaderField: URLRequest.noAppliedcookieHeaderKey) }
    }
}

/// https://github.com/Kofktu/WKCookieWebView/blob/master/WKCookieWebView/WKCookieWebView.swift
extension HTTPCookie {

    var javaScriptString: String {
        if var properties = properties {
            properties.removeValue(forKey: .name)
            properties.removeValue(forKey: .value)

            return properties.reduce(into: ["\(name)=\(value)"]) { result, property in
                result.append("\(property.key.rawValue)=\(property.value)")
            }.joined(separator: "; ")
        }

        var script = [
            "\(name)=\(value)",
            "domain=\(domain)",
            "path=\(path)"
        ]

        if isSecure { script.append("secure=true") }

        if let expiresDate = expiresDate {
            script.append("expires=\(HTTPCookie.dateFormatter.string(from: expiresDate))")
        }

        return script.joined(separator: "; ")
    }

    private static let dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US")
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
        return dateFormatter
    }()
}

Usage

Do not forget to paste the Solution code here

class WebViewController: UIViewController {
   
    private let host = "google.com"
    private weak var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
    }
    
    func setupWebView() {
        let cookies: [HTTPCookie] = []
        let configuration = WKWebViewConfiguration()
        configuration.websiteDataStore = .nonPersistent()
        configuration.set(cookies: cookies) {
            let webView = WKWebView(frame: .zero, configuration: configuration)
            /// ..
            self.webView = webView
            
            self.loadPage(url: URL(string:self.host)!)
        }
    }
    
    private func loadPage(url: URL) {
        var request = URLRequest(url: url)
        request.setCookies()
        webView.load(request)
    }
}

extension WebViewController: WKNavigationDelegate {

     // https://stackoverflow.com/a/47529039/4488252
     func webView(_ webView: WKWebView,
                  decidePolicyFor navigationAction: WKNavigationAction,
                  decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

         if #available(iOS 11.0, *) {
             decisionHandler(.allow)
         } else {
             guard let url = navigationAction.request.url, let host = url.host, host.contains(self.host) else {
                 decisionHandler(.allow)
                 return
             }

             if navigationAction.request.hasCookies {
                 decisionHandler(.allow)
             } else {
                 DispatchQueue.main.async {
                     decisionHandler(.cancel)
                     self.loadPage(url: url)
                 }
             }
         }
     }
 }

Full Sample

Do not forget to paste the Solution code here

import UIKit
import WebKit

class ViewController: UIViewController {

    private weak var webView: WKWebView!
    let url = URL(string: "your_url")!
    
    var cookiesData: [String : Any]  {
        [
            "access_token": "your_token"
        ]
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let configuration = WKWebViewConfiguration()
        
        guard let host = self.url.host else { return }
        configuration.set(cookies: createCookies(host: host, parameters: self.cookiesData)) {
            let webView = WKWebView(frame: .zero, configuration: configuration)
            self.view.addSubview(webView)
            self.webView = webView
            webView.navigationDelegate = self
            webView.translatesAutoresizingMaskIntoConstraints = false
            webView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
            webView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
            self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor).isActive = true
            self.view.rightAnchor.constraint(equalTo: webView.rightAnchor).isActive = true

            self.loadPage(url: self.url)
        }
    }

    private func loadPage(url: URL) {
        var request = URLRequest(url: url)
        request.timeoutInterval = 30
        request.setCookies()
        webView.load(request)
    }
    
    private func createCookies(host: String, parameters: [String: Any]) -> [HTTPCookie] {
        parameters.compactMap { (name, value) in
            HTTPCookie(properties: [
                .domain: host,
                .path: "/",
                .name: name,
                .value: "\(value)",
                .secure: "TRUE",
                .expires: Date(timeIntervalSinceNow: 31556952),
            ])
        }
    }
}

extension ViewController: WKNavigationDelegate {

    // https://stackoverflow.com/a/47529039/4488252
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        if #available(iOS 11.0, *) {
            decisionHandler(.allow)
        } else {
            guard let url = navigationAction.request.url, let host = url.host, host.contains(self.url.host!) else {
                decisionHandler(.allow)
                return
            }

            if navigationAction.request.hasCookies {
                decisionHandler(.allow)
            } else {
                DispatchQueue.main.async {
                    decisionHandler(.cancel)
                    self.loadPage(url: url)
                }
            }
        }
    }
}

Info.plist

add in your Info.plist transport security setting

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
1
votes

Finally got the solution which is working on ios 11+. Pasting my code here...

extension WKWebViewConfiguration {

static func includeCookie(preferences:WKPreferences, completion: @escaping (WKWebViewConfiguration?) -> Void) {
    let config = WKWebViewConfiguration()
    
    guard let cookies = HTTPCookieStorage.shared.cookies else {
        completion(config)
        return
    }
    
    config.preferences = preferences
    let dataStore = WKWebsiteDataStore.nonPersistent()
    HTTPCookieStorage.shared.cookieAcceptPolicy = .always
    
    DispatchQueue.main.async {
        let waitGroup = DispatchGroup()
        
        for cookie in cookies{
            waitGroup.enter()
            let customCookie = HTTPCookie(properties: [
                .domain: cookie.domain,
                .path: cookie.path,
                .name: cookie.name,
                .value: cookie.value,
                .secure: cookie.isSecure,
                .expires: cookie.expiresDate ?? NSDate(timeIntervalSinceNow: 31556926)
            ])
            if let cookieData = customCookie{
                dataStore.httpCookieStore.setCookie(cookieData) {
                    waitGroup.leave()
                }
            }
        }
        
        waitGroup.notify(queue: DispatchQueue.main) {
            config.websiteDataStore = dataStore
            completion(config)
        }
    }
}
}

After setting the cookie in WKWebViewConfiguration, use the same config to load the webview...

WKWebViewConfiguration.includeCookie(preferences: preferences, completion: {
            [weak self] config in
    if let `self` = self {
        if let configuration = config {
            webview = WKWebView(frame: self.contentView.bounds, configuration: config)
            webview.configuration.websiteDataStore.httpCookieStore.getAllCookies { (response) in
                print("")
            }

            self.contentView.addSubview(webview)

            if let filePath = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "packageDetailWevview") {

                if let requestUrl = filePath {
                    let request = URLRequest(url: requestUrl)
                    webview.load(request)
                }
            }
        }
    }
})
0
votes

The better fix for XHR requests is shown here

Swift 4 version:

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Swift.Void) {
    guard
        let response = navigationResponse.response as? HTTPURLResponse,
        let url = navigationResponse.response.url
    else {
        decisionHandler(.cancel)
        return
    }

    if let headerFields = response.allHeaderFields as? [String: String] {
        let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
        cookies.forEach { (cookie) in
            HTTPCookieStorage.shared.setCookie(cookie)
        }
    }

    decisionHandler(.allow)
}
0
votes

If anyone is using Alamofire, then this is better solution.

  let cookies = Alamofire.SessionManager.default.session.configuration.httpCookieStorage?.cookies(for: URL(string: BASE_URL)!)
  for (cookie) in cookies ?? [] {
      webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
  }
0
votes

This works for me: After setcookies , add fetchdatarecords

   let cookiesSet = NetworkProvider.getCookies(forKey : 
    PaywallProvider.COOKIES_KEY, completionHandler: nil)
                let dispatchGroup = DispatchGroup()
                for (cookie) in cookiesSet {
                    if #available(iOS 11.0, *) {
                        dispatchGroup.enter()
                        self.webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie){
                            dispatchGroup.leave()
                            print ("cookie added: \(cookie.description)")
                            }
                        } else {
                                            // TODO Handle ios 10 Fallback on earlier versions
                        }
                    }
                    dispatchGroup.notify(queue: .main, execute: {


    self.webView.configuration.websiteDataStore.fetchDataRecords(ofTypes: 
    WKWebsiteDataStore.allWebsiteDataTypes()) { records in
                            records.forEach { record in

                                print("[WebCacheCleaner] Record \(record)")
                            }
                            self.webView.load(URLRequest(url: 
    self.dataController.premiumArticleURL , 
    cachePolicy:NSURLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData,
                                                         timeoutInterval: 10.0))
                        }

                    })
                }
0
votes

When adding multiply cookie items, you can do it like this: (path & domain is required for each item)

NSString *cookie = [NSString stringWithFormat:@"document.cookie = 'p1=%@;path=/;domain=your.domain;';document.cookie = 'p2=%@;path=/;domain=your.domain;';document.cookie = 'p3=%@;path=/;domain=your.domain;';", p1_string, p2_string, p3_string];

WKUserScript *cookieScript = [[WKUserScript alloc]
            initWithSource:cookie
            injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];

[userContentController addUserScript:cookieScript];

otherwise, only the first cookie item will be set.

0
votes

You can also use WKWebsiteDataStore to get similar behaviour to HTTPCookieStorage from UIWebView.

let dataStore = WKWebsiteDataStore.default()
let cookies = HTTPCookieStorage.shared.cookies ?? [HTTPCookie]()
cookies.forEach({
    dataStore.httpCookieStore.setCookie($0, completionHandler: nil)
})
0
votes

Below code is work well in my project Swift5. try load url by WKWebView below:

    private func loadURL(urlString: String) {
        let url = URL(string: urlString)
        guard let urlToLoad = url else { fatalError("Cannot find any URL") }

        // Cookies configuration
        var urlRequest = URLRequest(url: urlToLoad)
        if let cookies = HTTPCookieStorage.shared.cookies(for: urlToLoad) {
            let headers = HTTPCookie.requestHeaderFields(with: cookies)
            for header in headers { urlRequest.addValue(header.value, forHTTPHeaderField: header.key) }
        }

        webview.load(urlRequest)
    }
0
votes

This is my solution to handle with Cookies and WKWebView in iOS 9 or later.

import WebKit

extension WebView {

    enum LayoutMode {
        case fillContainer
    }

    func autoLayout(_ view: UIView?, mode: WebView.LayoutMode = .fillContainer) {
        guard let view = view else { return }
        self.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(self)

        switch mode {
        case .fillContainer:
                NSLayoutConstraint.activate([
                self.topAnchor.constraint(equalTo: view.topAnchor),
                self.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                self.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                self.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ])
        }
    }

}

class WebView : WKWebView {

    var request : URLRequest?

    func load(url: URL, useSharedCookies: Bool = false) {
        if useSharedCookies, let cookies = HTTPCookieStorage.shared.cookies(for: url) {
            self.load(url: url, withCookies: cookies)
        } else {
            self.load(URLRequest(url: url))
        }
    }

    func load(url: URL, withCookies cookies: [HTTPCookie]) {
        self.request = URLRequest(url: url)
        let headers = HTTPCookie.requestHeaderFields(with: cookies)
        self.request?.allHTTPHeaderFields = headers
        self.load(request!)
    }

}
0
votes

This mistake i was doing is i was passing the whole url in domain attribute, it should be only domain name.

let cookie = HTTPCookie(properties: [
.domain: "example.com",
.path: "/",
.name: "MyCookieName",
.value: "MyCookieValue",
.secure: "TRUE",
])! 

webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
0
votes

Here is how I am doing this-

call initWebConfig in didFinishLaunchingWithOptions of AppDelegate (or anywhere before creating the WebView) otherwise sometimes Cookies do not sync properly-

    func initWebConfig() {
        self.webConfig = WKWebViewConfiguration()
        self.webConfig.websiteDataStore = WKWebsiteDataStore.nonPersistent()
    }
           
    func setCookie(key: String, value: AnyObject, domain: String? = nil, group: DispatchGroup? = nil) {
                        
                        let cookieProps: [HTTPCookiePropertyKey : Any] = [
                            .domain: domain ?? "google.com",
                            .path: "/",
                            .name: key,
                            .value: value,
                        ]
                        
                        if let cookie = HTTPCookie(properties: cookieProps) {
                            group?.enter()
                            let webConfig = (UIApplication.shared.delegate as? AppDelegate)?.webConfig
          
webConfig?.websiteDataStore.httpCookieStore.setCookie(cookie) {
                                group?.leave()
                            }
                        }
                    }

Where required, set cookies in dispatch group-

 let group = DispatchGroup()
                self.setCookie(key: "uuid", value: "tempUdid" as AnyObject, group: group)
                self.setCookie(key: "name", value: "tempName" as AnyObject, group: group)
                
                group.notify(queue: DispatchQueue.main) {
                    //Create and Load WebView here 
                    let webConfig = (UIApplication.shared.delegate as? AppDelegate)?.webConfig ?? WKWebViewConfiguration()
                    //create urlRequest
                    let webView = WKWebView(frame: .zero, configuration: webConfig)
                    self.webView.load(urlRequest)
                }