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 .
You can share cookies between multiple WKWebView 's inside your app by utilising WKProcessPool . There is a way of passing cookie data from Safari to your app by combining SFSafariViewController (iOS 8 and below you will need to switch to Safari) with a custom URL scheme.
Difference Between UIWebview and WKWebViewUIWebview is a part of UIKit, so it is available to your apps as standard. You don't need to import anything, it will we there by default. But WKWebView is run in a separate process to your app,. You need to import Webkit to use WKWebView in your app.
Since then, we've recommended that you adopt WKWebView instead of UIWebView and WebView — both of which were formally deprecated. New apps containing these frameworks are no longer accepted by the App Store.
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!
After playing with this answer (which was fantastically helpful :) we've had to make a few changes:
NSHTTPCookieStorage
So we modified our code to be this;
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.
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];
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;
}
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.
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)
}
}
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With