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?
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.
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;
}
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
}
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()
}
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)
}
}
In iOS 11, you can manage cookie now :), see this session: https://developer.apple.com/videos/play/wwdc2017/220/
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!
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
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)
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>
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)
}
}
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)
}
}
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)
})
}
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)
}
}
}
}
})
finally got it working in swift 5.
extension WebController{
func save_cookies(){
let cookieStore = self.webView.configuration.websiteDataStore.httpCookieStore
cookieStore.getAllCookies { (cookies) in
let array = cookies.compactMap { (cookie) -> [HTTPCookiePropertyKey: Any]? in
cookie.properties
}
UserDefaults.standard.set(array, forKey: "cookies")
}
}
func load_cookies(){
// get status from cookies
// cookies are pre-installed from native code.
guard let cookies = UserDefaults.standard.value(forKey: "cookies") as? [[HTTPCookiePropertyKey: Any]] else {
return
}
cookies.forEach { (cookie) in
guard let cookie = HTTPCookie(properties: cookie ) else{return}
let cookieStore = self.webView.configuration.websiteDataStore.httpCookieStore
cookieStore.setCookie(cookie, completionHandler: nil)
}
webView.evaluateJavaScript("checkcookie_delay_1second()", completionHandler: nil)
}
}
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)
}
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)
}
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))
}
})
}
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.
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)
})
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)
}
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!)
}
}
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)
}
Related
I'm showing html page in a WKWebView. My html contains links with embedded pdf as follow:
<td><div class="truncate"><a id="allegato1" class="link" href="data:octet-stream;base64,JVBERi0xLjIgCiXi48/
.........................................................
Ao3OTA2MiAKJSVFT0YgCg==%0A" download="FATCLI_19244324.PDF">FATCLI_19244324.PDF</a></div></td>
Now, I have to intercept click in the above links, save pdf on disk and then open the file with a reader. I'm able to do this as follow:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Swift.Void) {
if navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url {
if url.scheme == "data" {
let splitted = url.absoluteString.components(separatedBy: "base64,")
if splitted.count == 2 {
let documentName = .... // What should I do?
if let data = Data(base64Encoded: splitted[1]) {
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(documentName)
do {
try data.write(to: fileURL, options: .atomicWrite)
// Show pdf with via UIDocumentInteractionController
decisionHandler(.cancel)
return
} catch {
// Manage the error
decisionHandler(.cancel)
return
}
}
}
}
decisionHandler(.allow)
} else {
decisionHandler(.allow)
}
}
What I am trying to do is find a way to read document name from element download or from tag a value (FATCLI_19244324.PDF in this case). But it seems to be no way to do this in webView(_:decidePolicyFor:decisionHandler:)method.
Can anyone help me?
I solved my problem by injecting some javascript in my WKWebView.
override func viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
let userController:WKUserContentController = WKUserContentController()
userController.add(self, name: "linkClicked")
let js:String = """
var links = document.getElementsByClassName("link");
Array.prototype.forEach.call(links, function(link) {
// Disable long press
link.style.webkitTouchCallout='none';
link.addEventListener("click", function() {
var messageToPost = {'download': link.getAttribute("download"), 'href': link.getAttribute("href")}; window.webkit.messageHandlers.linkClicked.postMessage(messageToPost);
},false);
});
"""
let userScript:WKUserScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
userController.addUserScript(userScript)
WKUserContentController
configuration.userContentController = userController;
myWebView = WKWebView(frame: webViewContainer.bounds, configuration: configuration)
......
}
In this way I can read download and href elements in userContentController(_:didReceive:) method and save pdf on disk with right name.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let body = message.body as? [String: Any] else { return }
guard let documentName = body["download"] as? String, let href = body["href"] as? String, let url = URL(string: href) else { return }
if url.scheme == "data" {
let splitted = url.absoluteString.components(separatedBy: "base64,")
if splitted.count == 2 {
if let data = Data(base64Encoded: splitted[1]) {
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(documentName)
do {
try data.write(to: fileURL, options: .atomicWrite)
// Show pdf with via UIDocumentInteractionController
} catch {
// Manage the error
}
}
}
}
}
I have a question about WKWebview.
First, I have a UITabbarViewController(tab1, tab2), and two ViewControllers.
In ViewController1 have two button (button1, button2) which action is click the tabbar tab2 to present the ViewController2 and bring different url string.
In ViewController2 have a WKWebView, and load the request of url string.
I work fine in simulator, but build in device and work fine at first time and the then I tap the tabbar tab1 and back to ViewController1.
I click the button2 and present the ViewController2.
And I also set breakpoint, the WKWebview have run the load request function.
But the WKWebview also show me the button1 url content, not button2 url content. When I back and click button2 again, it's work. Why the WKWebView don't refresh at first time?
Thanks.
Xcode 9.4.1, device iOS 12.0.1(16A404)
class ViewController1: UIViewController {
#objc func btnTapped(sender: UIButton) {
if let vc = self.navigationController?.parent as? ViewController {
if let number = item?.number {
let domainPath = “\(customURL!.absoluteString)"
let extPath = "detail/\(number)/&topPageBack=list"
let encodestr = extPath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let finalPath = domainPath.appending("/").appending(encodestr!)
if let url = URL(string: finalPath) {
vc.loadSpecificURL = url
}
vc.tabBarView.buttonClick(tag: 1)
}
}
}
}
class ViewController2: UIViewController {
lazy var webViewController: CustomWKWebViewController = {
let controller = CustomWKWebViewController()
controller.wKWebViewPage = .searchPage
return controller
}()
var loadURL: URL?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let url = loadURL {
webViewController.reloadWebView(url: url)
loadURL = nil
}
}
}
class CustomWKWebViewController: UIViewController {
private func settingWebView() -> WKWebView {
let userScript = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true)
let userContentController = WKUserContentController()
userContentController.add(self, name: "Login")
userContentController.add(self, name: "Other")
userContentController.add(self, name: “LD”)
userContentController.addUserScript(userScript)
let config = WKWebViewConfiguration()
config.userContentController = userContentController
webView = WKWebView(frame: .zero, configuration: config)
webView!.navigationDelegate = self
webView!.uiDelegate = self
webView!.scrollView.bounces = false
webView!.allowsBackForwardNavigationGestures = true
webView!.backgroundColor = .clear
webView!.scrollView.backgroundColor = .clear
if let request = getRequest() {
webView!.load(request)
}
return webView!
}
func reloadWebView(url: URL?) {
if let url = url, let webView = self.webView {
var requestForBlank = URLRequest(url: URL(string: "about:blank")!, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 3)
requestForBlank.httpMethod = "POST"
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 3)
request.httpMethod = "POST"
webView.load(requestForBlank)
webView.load(request)//breakpoint always break here, but the view don't load request.
}
}
}
First you check if web url not nil, then also call the else part of if.
if let url = webview.url {
webview.reload()
} else {
webview.load(URLRequest(url: originalURL))
}
I am desperately trying to add a custom cookie to a WKWebView instance (without using Javascript or similar workarounds).
From iOS 11 and upwards, Apple provides an API to do this: The WKWebViews WKWebsiteDataStore has a property httpCookieStore.
Here is my (example) code:
import UIKit
import WebKit
class ViewController: UIViewController {
var webView: WKWebView!
override func viewDidLoad() {
webView = WKWebView()
view.addSubview(webView)
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let cookie = HTTPCookie(properties: [
HTTPCookiePropertyKey.domain : "google.com",
HTTPCookiePropertyKey.path : "/",
HTTPCookiePropertyKey.secure : true,
HTTPCookiePropertyKey.name : "someCookieKey",
HTTPCookiePropertyKey.value : "someCookieValue"])!
let cookieStore = webView.configuration.websiteDataStore.httpCookieStore
cookieStore.setCookie(cookie) {
DispatchQueue.main.async {
self.webView.load(URLRequest(url: URL(string: "https://google.com")!))
}
}
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
webView.frame = view.bounds
}
}
After this, if I use webView.configuration.websiteDataStore.httpCookieStore.getAllCookies(completionHandler:) I see that my cookie is in the list of cookies.
However, when inspecting the webview using Safari's developer tools (using a iOS Simulator of course) the cookie does not show up.
I also tried to inspect the traffic using a HTTP proxy (Charles in my case) to see if the cookie in included in my HTTP requests. It is not.
What am I doing wrong here? How can I add a cookie to WKWebView (on iOS versions 11 and up)?
A bit late but I want to share a solution that worked for me, I hope it helps someone facing the same issue on iOS 12 as well.
Here is the simplified workflow I used:
Instantiate a WKWebsiteDataStore object
Set the custom cookie to its httpCookieStore
Wait for the cookie to be set
Instantiate the WKWebView
Load the request
To do that I have created an extension of WKWebViewConfiguration:
extension WKWebViewConfiguration {
static func includeCookie(cookie:HTTPCookie, preferences:WKPreferences, completion: #escaping (WKWebViewConfiguration?) -> Void) {
let config = WKWebViewConfiguration()
config.preferences = preferences
let dataStore = WKWebsiteDataStore.nonPersistent()
DispatchQueue.main.async {
let waitGroup = DispatchGroup()
waitGroup.enter()
dataStore.httpCookieStore.setCookie(cookie) {
waitGroup.leave()
}
waitGroup.notify(queue: DispatchQueue.main) {
config.websiteDataStore = dataStore
completion(config)
}
}
}
And for my example I have used it as follows:
override func viewDidLoad() {
self.AddWebView()
}
private func addWebView(){
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
preferences.javaScriptCanOpenWindowsAutomatically = true
let cookie = HTTPCookie(properties: [
.domain: COOKIE_DOMAIN,
.path: "/",
.name: COOKIE_NAME,
.value: myCookieValue,
.secure: "TRUE",
.expires: NSDate(timeIntervalSinceNow: 3600)
])
//Makes sure the cookie is set before instantiating the webview and initiating the request
if let myCookie = cookie {
WKWebViewConfiguration.includeCookie(cookie: myCookie, preferences: preferences, completion: {
[weak self] config in
if let `self` = self {
if let configuration = config {
self.webView = WKWebView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width , height: self.view.frame.height), configuration: configuration)
self.view.addSubview(self.webView)
self.webView.load(self.customRequest)
}
}
}
}
Any request to google.com redirects to www.google.com.
You would need to add www. to the domain field of the cookie. If the domain or the path doesn't match the request, the cookie won't be sent.
You can add the cookies explicitly.
let url = URL(string: "https://www.google.com")!
var request = URLRequest(url: url)
if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
request.allHTTPHeaderFields = HTTPCookie.requestHeaderFields(with: cookies)
}
self.webView.load(request)
For iOS 11+ you really don't need to worry about cookies part it is very simple. Create your cookie like this. Don't make it secure true
let newcookie = HTTPCookie(properties: [
.domain: "domain",
.path: "/",
.name: "name",
.value: "vale",
.secure: "FALSE",
.expires: NSDate(timeIntervalSinceNow: 31556926)
])!
self.webview.configuration.websiteDataStore.httpCookieStore.setCookie(newcookie, completionHandler: {
// completion load your url request here. Better to add cookie in Request as well. like this way
request.addCookies()
//enable cookie through request
request.httpShouldHandleCookies = true
//load request in your webview.
})
Request extention add after cookie value ";"
extension URLRequest {
internal mutating func addCookies() {
var cookiesStr: String = ""
let mutableRequest = ((self as NSURLRequest).mutableCopy() as? NSMutableURLRequest)!
cookiesStr += cookie.name + "=" + cookie.value + ";"
mutableRequest.setValue(cookiesStr, forHTTPHeaderField: "Cookie")
self = mutableRequest as URLRequest
}
}
Also i can see your are not setting WKWebViewConfiguration of wkwebview. Set configuration of your wkwebview. Also implement WKHTTPCookieStoreObserver and implement function.
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
// Must implement otherwise wkwebview cookie not sync properly
self.httpCookieStore.getAllCookies { (cookies) in
cookies.forEach({ (cookie) in
})
}
}
Hope so this will work.
Everytime I try to load another URL on a UIWebView i get the same error:
SendDelegateMessage(NSInvocation *): delegate (webView:decidePolicyForNavigationAction:request:frame:decisionListener:) failed to return after waiting 10 seconds.
My code about the webView is like:
#IBAction func changeUrl(sender: UIButton) {
if LEDON {
let urlStr = NSString(format: "%#/?Led=0", self.url)
let req = URLRequest(url: URL(string: urlStr as String)!)
webView.loadRequest(req)
while(webView.isLoading) {
sender.isEnabled = false
}
sender.isEnabled = true
LEDON = false;
} else {
let urlStr = NSString(format: "%#/?Led=1", self.url)
let req = URLRequest(url: URL(string: urlStr as String)!)
webView.loadRequest(req)
while(webView.isLoading) {
sender.isEnabled = false
}
sender.isEnabled = true
LEDON = true;
}
}
If I press the UIButton the first time the code works perfectly, but when I tap it again I get that error.
Where am I doing wrong?
"url" variable in the code is a global var and I already check that is correct.
Problem in
while(webView.isLoading) {
sender.isEnabled = false
}
You have frozen main thread and webView:decidePolicyForNavigationAction:request:frame:decisionListener: can't be invoke.
Try to use this:
func viewDidLoad()
{
...
webView.delegate = self;//UIWebView
//webView. navigationDelegate = self;//WKWebView
}
#IBAction func changeUrl(sender: UIButton) {
if LEDON {
let urlStr = NSString(format: "%#/?Led=0", self.url)
let req = URLRequest(url: URL(string: urlStr as String)!)
webView.loadRequest(req)
sender.isEnabled = false
LEDON = false;
} else {
let urlStr = NSString(format: "%#/?Led=1", self.url)
let req = URLRequest(url: URL(string: urlStr as String)!)
webView.loadRequest(req)
sender.isEnabled = false
LEDON = true;
}
}
For UIWebView you need to use
func webViewDidFinishLoad(UIWebView)
{
yourButton.isEnabled = true
}
For WKWebView
func webView(_ webView: WKWebView,
didFinish navigation: WKNavigation!)
{
yourButton.isEnabled = true
}
======================================
If you want use while loop try to change it for this:
while(webView.isLoading) {
sender.isEnabled = false
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.5))
}
You need to allow http and https on your app :
In your info.plist :
In Info.plist file add a dictionary with key 'NSAppTransportSecurity'
Add another element inside dictionary with key 'Allow Arbitrary Loads'
We're trying to save the content (HTML) of WKWebView in a persistent storage (NSUserDefaults, CoreData or disk file). The user can see the same content when he re-enters the application with no internet connection. WKWebView doesn't use NSURLProtocol like UIWebView (see post here).
Although I have seen posts that "The offline application cache is not enabled in WKWebView." (Apple dev forums), I know that a solution exists.
I've learned of two possibilities, but I couldn't make them work:
1) If I open a website in Safari for Mac and select File >> Save As, it will appear the following option in the image below. For Mac apps exists [[[webView mainFrame] dataSource] webArchive], but on UIWebView or WKWebView there is no such API. But if I load a .webarchive file in Xcode on WKWebView (like the one I obtained from Mac Safari), then the content is displayed correctly (html, external images, video previews) if there is no internet connection. The .webarchive file is actually a plist (property list). I tried to use a mac framework that creates a .webarchive file, but it was incomplete.
2) I obtanined the HTML in webView:didFinishNavigation but it doesn't save external images, css, javascript
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()",
completionHandler: { (html: AnyObject?, error: NSError?) in
print(html)
})
}
We're struggling over a week and it is a main feature for us.
Any idea is really appreciated.
Thank you!
I know I'm late, but I have recently been looking for a way to store web pages for offline reading, and still could't find any reliable solution that wouldn't depend on the page itself and wouldn't use the deprecated UIWebView. A lot of people write that one should use the existing HTTP caching, but WebKit seems to do a lot of stuff out-of-process, making it virtually impossible to enforce complete caching (see here or here). However, this question guided me into the right direction. Tinkering with the web archive approach, I found that it's actually quite easy to write your own web archive exporter.
As written in the question, web archives are just plist files, so all it takes is a crawler that extracts the required resources from the HTML page, downloads them all and stores them in a big plist file. This archive file can then later be loaded into the WKWebView via loadFileURL(URL:allowingReadAccessTo:).
I created a demo app that allows archiving from and restoring to a WKWebView using this approach: https://github.com/ernesto-elsaesser/OfflineWebView
EDIT: The archive generation code is now available as standalone Swift package: https://github.com/ernesto-elsaesser/WebArchiver
The implementation only depends on Fuzi for HTML parsing.
I would recommend investigating the feasibility of using App Cache, which is now supported in WKWebView as of iOS 10: https://stackoverflow.com/a/44333359/233602
I'm not sure if you just want to cache the pages that have already been visited or if you have specific requests that you'd like to cache. I'm currently working on the latter. So I'll speak to that. My urls are dynamically generated from an api request. From this response I set requestPaths with the non-image urls and then make a request for each of the urls and cache the response. For the image urls, I used the Kingfisher library to cache the images. I've already set up my shared cache urlCache = URLCache.shared in my AppDelegate. And allotted the memory I need: urlCache = URLCache(memoryCapacity: <setForYourNeeds>, diskCapacity: <setForYourNeeds>, diskPath: "urlCache") Then just call startRequest(:_) for each of the urls in requestPaths. (Can be done in the background if it's not needed right away)
class URLCacheManager {
static let timeout: TimeInterval = 120
static var requestPaths = [String]()
class func startRequest(for url: URL, completionWithErrorCallback: #escaping (_ error: Error?) -> Void) {
let urlRequest = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: timeout)
WebService.sendCachingRequest(for: urlRequest) { (response) in
if let error = response.error {
DDLogError("Error: \(error.localizedDescription) from cache response url: \(String(describing: response.request?.url))")
}
else if let _ = response.data,
let _ = response.response,
let request = response.request,
response.error == nil {
guard let cacheResponse = urlCache.cachedResponse(for: request) else { return }
urlCache.storeCachedResponse(cacheResponse, for: request)
}
}
}
class func startCachingImageURLs(_ urls: [URL]) {
let imageURLs = urls.filter { $0.pathExtension.contains("png") }
let prefetcher = ImagePrefetcher.init(urls: imageURLs, options: nil, progressBlock: nil, completionHandler: { (skipped, failed, completed) in
DDLogError("Skipped resources: \(skipped.count)\nFailed: \(failed.count)\nCompleted: \(completed.count)")
})
prefetcher.start()
}
class func startCachingPageURLs(_ urls: [URL]) {
let pageURLs = urls.filter { !$0.pathExtension.contains("png") }
for url in pageURLs {
DispatchQueue.main.async {
startRequest(for: url, completionWithErrorCallback: { (error) in
if let error = error {
DDLogError("There was an error while caching request: \(url) - \(error.localizedDescription)")
}
})
}
}
}
}
I'm using Alamofire for the network request with a cachingSessionManager configured with the appropriate headers. So in my WebService class I have:
typealias URLResponseHandler = ((DataResponse<Data>) -> Void)
static let cachingSessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = cachingHeader
configuration.urlCache = urlCache
let cachingSessionManager = SessionManager(configuration: configuration)
return cachingSessionManager
}()
private static let cachingHeader: HTTPHeaders = {
var headers = SessionManager.defaultHTTPHeaders
headers["Accept"] = "text/html"
headers["Authorization"] = <token>
return headers
}()
#discardableResult
static func sendCachingRequest(for request: URLRequest, completion: #escaping URLResponseHandler) -> DataRequest {
let completionHandler: (DataResponse<Data>) -> Void = { response in
completion(response)
}
let dataRequest = cachingSessionManager.request(request).responseData(completionHandler: completionHandler)
return dataRequest
}
Then in the webview delegate method I load the cachedResponse. I use a variable handlingCacheRequest to avoid an infinite loop.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let reach = reach {
if !reach.isReachable(), !handlingCacheRequest {
var request = navigationAction.request
guard let url = request.url else {
decisionHandler(.cancel)
return
}
request.cachePolicy = .returnCacheDataDontLoad
guard let cachedResponse = urlCache.cachedResponse(for: request),
let htmlString = String(data: cachedResponse.data, encoding: .utf8),
cacheComplete else {
showNetworkUnavailableAlert()
decisionHandler(.allow)
handlingCacheRequest = false
return
}
modify(htmlString, completedModification: { modifiedHTML in
self.handlingCacheRequest = true
webView.loadHTMLString(modifiedHTML, baseURL: url)
})
decisionHandler(.cancel)
return
}
handlingCacheRequest = false
DDLogInfo("Currently requesting url: \(String(describing: navigationAction.request.url))")
decisionHandler(.allow)
}
Of course you'll want to handle it if there is a loading error as well.
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
DDLogError("Request failed with error \(error.localizedDescription)")
if let reach = reach, !reach.isReachable() {
showNetworkUnavailableAlert()
handlingCacheRequest = true
}
webView.stopLoading()
loadingIndicator.stopAnimating()
}
I hope this helps. The only thing I'm still trying to figure out is the image assets aren't being loaded offline. I'm thinking I'll need to make a separate request for those images and keep a reference to them locally. Just a thought but I'll update this when I have that worked out.
UPDATED with images loading offline with below code
I used the Kanna library to parse my html string from my cached response, find the url embedded in the style= background-image: attribute of the div, used regex to get the url (which is also the key for Kingfisher cached image), fetched the cached image and then modified the css to use the image data (based on this article: https://css-tricks.com/data-uris/), and then loaded the webview with the modified html. (Phew!) It was quite the process and maybe there is an easier way.. but I had not found it. My code is updated to reflect all these changes. Good luck!
func modify(_ html: String, completedModification: #escaping (String) -> Void) {
guard let doc = HTML(html: html, encoding: .utf8) else {
DDLogInfo("Couldn't parse HTML with Kannan")
completedModification(html)
return
}
var imageDiv = doc.at_css("div[class='<your_div_class_name>']")
guard let currentStyle = imageDiv?["style"],
let currentURL = urlMatch(in: currentStyle)?.first else {
DDLogDebug("Failed to find URL in div")
completedModification(html)
return
}
DispatchQueue.main.async {
self.replaceURLWithCachedImageData(inHTML: html, withURL: currentURL, completedCallback: { modifiedHTML in
completedModification(modifiedHTML)
})
}
}
func urlMatch(in text: String) -> [String]? {
do {
let urlPattern = "\\((.*?)\\)"
let regex = try NSRegularExpression(pattern: urlPattern, options: .caseInsensitive)
let nsString = NSString(string: text)
let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range) }
}
catch {
DDLogError("Couldn't match urls: \(error.localizedDescription)")
return nil
}
}
func replaceURLWithCachedImageData(inHTML html: String, withURL key: String, completedCallback: #escaping (String) -> Void) {
// Remove parenthesis
let start = key.index(key.startIndex, offsetBy: 1)
let end = key.index(key.endIndex, offsetBy: -1)
let url = key.substring(with: start..<end)
ImageCache.default.retrieveImage(forKey: url, options: nil) { (cachedImage, _) in
guard let cachedImage = cachedImage,
let data = UIImagePNGRepresentation(cachedImage) else {
DDLogInfo("No cached image found")
completedCallback(html)
return
}
let base64String = "data:image/png;base64,\(data.base64EncodedString(options: .endLineWithCarriageReturn))"
let modifiedHTML = html.replacingOccurrences(of: url, with: base64String)
completedCallback(modifiedHTML)
}
}
Easiest way to use cache webpage is as following in Swift 4.0: -
/* Where isCacheLoad = true (Offline load data) &
isCacheLoad = false (Normal load data) */
internal func loadWebPage(fromCache isCacheLoad: Bool = false) {
guard let url = url else { return }
let request = URLRequest(url: url, cachePolicy: (isCacheLoad ? .returnCacheDataElseLoad: .reloadRevalidatingCacheData), timeoutInterval: 50)
//URLRequest(url: url)
DispatchQueue.main.async { [weak self] in
self?.webView.load(request)
}
}