How can I get failed URL from didFailProvisionalNavigation method - ios

I'm trying to display error message for my web view, and I need to know url which is not available, so I implemented delegate method:
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
[self.addressBar finishLoadingProgressAnimated:YES];
NSLog(#"%#", webView.URL);
[self showErrorPageForURL:error.userInfo[NSErrorFailingURLStringKey]];
}
but NSErrorFailingURLStringKey is deprecated, so how can I get failed URL?
WKNavigation's interface is empty. webView.URL == nil at that moment.

Swift 3 or Swift 4
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if error._domain == "WebKitErrorDomain" {
if let info = error._userInfo as? [String: Any] {
if let url = info["NSErrorFailingURLKey"] as? URL {
}
if let urlString = info["NSErrorFailingURLStringKey"] as? String {
}
}
}
}

You can use NSURLErrorFailingURLStringErrorKey to replace NSErrorFailingURLStringKey. If you jump to its definition in Xcode, you will find the below discussion.
This constant supersedes NSErrorFailingURLStringKey, which was
deprecated in Mac OS X 10.6. Both constants refer to the same value
for backward-compatibility, but this symbol name has a better prefix.

Related

Write Unit Tests to validate WKWebView load request

I have a WKWebView to loads a basic url request.
extension ViewController: WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate {
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
if url.description.lowercased().range(of: "http://") != nil ||
url.description.lowercased().range(of: "https://") != nil ||
url.description.lowercased().range(of: "mailto:") != nil {
UIApplication.shared.open(url)
}
}
return nil
}
func openSite() {
guard let myUrl = URL(string: "https://www.myurl.com") else { return }
let request = URLRequest(url: myUrl)
webView?.load(request)
self.webView.sizeToFit()
}
Now I want to write a unit test to verify webview correctly load the request. I have followed this approach https://stackoverflow.com/a/63827560/627667 and created a mock navigation action.
func test_AllowsCorrectURL() {
let action = MockNavigationAction()
action.mockedRequest = URLRequest(url: URL(string: "https://www.myurl")!)
let allowExpectation = expectation(description: "Allows action")
viewController.webView(WKWebView(), decidePolicyFor: action) { policy in
XCTAssertEqual(policy, .allow)
allowExpectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
However on this line viewController.webView(WKWebView(), decidePolicyFor: action) I am getting below error.
Cannot call value of non-function type 'WKWebView?'
Swift version 5. How to get rid of this error? Your suggestions highly appreciated.
I'm almost sure that this is related to you adding implementation for another method of WKNavigationDelegate to your vc. It has few of them containing decidePolicyFor parameter label and if you will omit the implementation for the one that you are trying to call in here you won't be able to do so.
The reason of that (to my understanding) is that these are methods marked as optional on the protocol so your class doesn't need to implement them and if compiler won't find implementation of the method that you try to call it will complete with the error.
Since you shared a concrete example, you're basing your solution on check if your vc is having a method with exactly this signature:
extension ViewController: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// ...
}
}

why I can’t open a App Store link inside a web view?

I'm getting always this error :
WebPageProxy::didFailProvisionalLoadForFrame: frameID=3, domain=WebKitErrorDomain, code=102
Normal links are working but the AppStore one is not working
what I want is the Link to open the AppStore I can't do it locally because the web is loaded from a Qualtrics web.
I try it adding the navigationAction function but that doesn't work, what I'm guessing is that maybe the request is taking some time and i need a way of load that data on an async way but to be honest i really dont know
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let html = """
Appstore link dont open</span></span><br />
Normal link </span></span><br />
"""
var loadStatusChanged: ((Bool, Error?) -> Void)? = nil
func makeCoordinator() -> WebView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
view.loadHTMLString(html, baseURL: nil)
return view
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
}
}
struct ContentView: View {
var body: some View {
WebView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Some links on tapping them might activate actions / redirect with url schemes that are non HTTPs like
_blank to open a new tab
mailto to launch the mail application
some other deep link techniques familiar to device OSs
I believe the app store link uses a combination of the above and WKWebView cannot handle non HTTPs schemes.
What you can do is to listen to URLs that fail using WKNavigationDelegate and handle them accordingly
I am not using SwiftUI but I think you can get the picture.
Set up using the same HTML as you with both the links
class ViewController: UIViewController, WKNavigationDelegate
{
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
let html = """
Appstore link dont open</span></span><br />
Normal link </span></span><br />
"""
let webview = WKWebView()
webview.frame = view.bounds
webview.navigationDelegate = self
view.addSubview(webview)
webview.loadHTMLString(html, baseURL: nil)
}
}
Then I implement these WKNavigationDelegate functions
decidePolicyFor navigationAction (documentation link) to allow even urls that do not follow the HTTPs scheme to be allowed to be processed
this navigation fail delegate function webView didFailProvisionalNavigation and check if iOS can handle the open in a new tab, mail, deep link etc so in your case it would open the app store
You could also implement the same logic as point 2 in this
WKNavigationDelegate function just in case
// MARK: WKNavigationDelegates
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
{
decisionHandler(.allow)
}
func webView(_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error)
{
manageFailedNavigation(webView,
didFail: navigation,
withError: error)
}
func webView(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error)
{
manageFailedNavigation(webView,
didFail: navigation,
withError: error)
}
private func manageFailedNavigation(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error)
{
// Check if this failed because of mailto, _blank, deep links etc
// I have commented out how to check for a specific case like open in a new tab,
// you can try to handle each case as you wish
if error.localizedDescription
== "Redirection to URL with a scheme that is not HTTP(S)"
//let url = webView.url, url.description.lowercased().range(of: "blank") != nil
{
// Convert error to NSError so we can access the url
let nsError = error as NSError
// Get the url from the error
// This key could change in future iOS releases
if let failedURL = nsError.userInfo["NSErrorFailingURLKey"] as? URL
{
// Check if the action can be handled by iOS
if UIApplication.shared.canOpenURL(failedURL)
{
// Request iOS to open handle the link
UIApplication.shared.open(failedURL, options: [:],
completionHandler: nil)
}
}
}
}
Give this a go and check if this fixes your issue. On my side, both links seem to work fine:

httpCookieStore is only available on iOS 11.0 or newer [duplicate]

while getting cookies from UIWebView seems straightforward by using NSHTTPCookieStorage.sharedHTTPCookieStorage(), it seems WKWebView stores the cookies somewhere else.
I did some research, and I was able to get some cookies from the grabbing it from NSHTTPURLResponse object. this, however, does not contain all the cookies used by WKWebView:
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let httpResponse = navigationResponse.response as? NSHTTPURLResponse {
if let headers = httpResponse.allHeaderFields as? [String: String], url = httpResponse.URL {
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url)
for cookie in cookies {
logDebug(cookie.description)
logDebug("found cookie " + cookie.name + " " + cookie.value)
}
}
}
}
Strangely, there's also a class WKWebsiteDataStore in ios 9 that responsible for managing cookies in WKWebView, however, the class does not contain a public method to retrieve the cookies data:
let storage = WKWebsiteDataStore.defaultDataStore()
storage.fetchDataRecordsOfTypes([WKWebsiteDataTypeCookies], completionHandler: { (records) -> Void in
for record in records {
logDebug("cookie record is " + record.debugDescription)
for dataType in record.dataTypes {
logDebug("data type is " + dataType.debugDescription)
// get cookie data??
}
}
})
Is there a workaround for getting the cookie data?
Cookies used (created) by the WKWebView are actually correctly stored in the NSHTTPCookieStorage.sharedHTTPCookieStorage().
The problem is that the WKWebView does not write back the cookies immediately. I think it does this on its own schedule. For example when a WKWebView is closed or maybe periodically.
So eventually they do end up in there, but when is unpredictable.
You may be able to force a 'sync' to the shared NSHTTPCookieStorage by closing your WKWebView. Please let us know if this works.
Update: I just remembered that in Firefox for iOS we force the WKWebView to flush its internal data, including cookies, by replacing its WKProcessPool with a new one. There is no official API, but I am pretty sure that is the most reliable workaround right now.
Finally, httpCookieStore for WKWebsiteDataStore landed in iOS 11.
https://developer.apple.com/documentation/webkit/wkwebsitedatastore?changes=latest_minor
Details
Xcode 9.2, Swift 4
Xcode 10.2 (10E125), Swift 5
Solution
extension WKWebView {
private var httpCookieStore: WKHTTPCookieStore { return WKWebsiteDataStore.default().httpCookieStore }
func getCookies(for domain: String? = nil, completion: #escaping ([String : Any])->()) {
var cookieDict = [String : AnyObject]()
httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
if let domain = domain {
if cookie.domain.contains(domain) {
cookieDict[cookie.name] = cookie.properties as AnyObject?
}
} else {
cookieDict[cookie.name] = cookie.properties as AnyObject?
}
}
completion(cookieDict)
}
}
}
Usage
// get cookies for domain
webView.getCookies(for: url.host) { data in
print("=========================================")
print("\(url.absoluteString)")
print(data)
}
// get all cookies
webView.getCookies() { data in
print("=========================================")
print("\(url.absoluteString)")
print(data)
}
Full sample
Info.plist
add in your Info.plist transport security setting
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Code
Do not forget to add the solution code here
ViewController has embed view controller
import UIKit
import WebKit
class ViewController: UIViewController {
private lazy var url = URL(string: "https://google.com")!
private weak var webView: WKWebView?
func initWebView(configuration: WKWebViewConfiguration) {
if webView != nil { return }
let webView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)
self.webView = webView
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if webView == nil { initWebView(configuration: WKWebViewConfiguration()) }
webView?.load(url: url)
}
}
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if let url = webView.url {
webView.getCookies(for: url.host) { data in
print("=========================================")
print("\(url.absoluteString)")
print(data)
}
}
}
}
extension ViewController: WKUIDelegate {
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
// push new screen to the navigation controller when need to open url in another "tab"
if let url = navigationAction.request.url, navigationAction.targetFrame == nil {
let viewController = ViewController()
viewController.initWebView(configuration: configuration)
viewController.url = url
DispatchQueue.main.async { [weak self] in
self?.navigationController?.pushViewController(viewController, animated: true)
}
return viewController.webView
}
return nil
}
}
extension WKWebView {
func load(urlString: String) {
if let url = URL(string: urlString) { load(url: url) }
}
func load(url: URL) { load(URLRequest(url: url)) }
}
I know this is a very old question, and we have a solution but work only on iOS 11 and upper. For those one who are dealing with iOS 10 and lower (like me), you may consider this method. It works perfectly to me:
Force reset processPool:
extension WKWebView {
func refreshCookies() {
self.configuration.processPool = WKProcessPool()
// TO DO: Save your cookies,...
}
}
--> this only work on real device.
For simulator, you should add:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
if let response = navigationResponse.response as? HTTPURLResponse,
let allHttpHeaders = response.allHeaderFields as? [String: String],
let responseUrl = response.url {
let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHttpHeaders, for: responseUrl)
for cookie in cookies {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
decisionHandler(.allow)
}
Follow to the answer of Stefan Arentz and Phenom.
For iOS 11, without any extensions:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
//...
}
}
}
if (#available(iOS 11.0, *)) {
[webView.configuration.websiteDataStore.httpCookieStore
getAllCookies:^(NSArray<NSHTTPCookie *> *_Nonnull cookies) {
NSURLRequest *request =
[[NSURLRequest alloc] initWithURL:self.URL]; //your URL
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session
dataTaskWithRequest:request
completionHandler:^(NSData *responseData, NSURLResponse *response,
NSError *error) {
//Do Something
}];
[task resume];
[session.configuration.HTTPCookieStorage storeCookies:cookies forTask:task];
}];
}
I used WKHTTPCookieStore in Objective-C, This worked for me to get both persistent and session cookies, but it only works in iOS 11+
https://developer.apple.com/documentation/webkit/wkhttpcookiestore?changes=latest_minor&language=objc
if (#available(iOS 11.0, *)) {
WKHTTPCookieStore *cookieStore = _webView.configuration.websiteDataStore.httpCookieStore;
[cookieStore getAllCookies:^(NSArray* cookies) {
NSHTTPCookie *cookie;
for(cookie in cookies){
NSLog(#"cookie: %#", cookie);
}
}];
Forcing the WKWebView to flush its internal data by replacing its WKProcessPool as described by Stefan's answer worked for me in iOS 10 and 11 but only for persistent cookies; it seems like session cookies get removed, as J. Thoo described
Swift 5
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
debugPrint(cookies.debugDescription)
}
decisionHandler(.allow)
}
As Stefan mentioned, cookies are stored in
NSHTTPCookieStorage.sharedHTTPCookieStorage()
However, from my experiments, I found that Session cookies set by the server are not visible to NSHTTPCookieStorage.sharedHTTPCookieStorage().
As long as each WKWebView share the same instance of WKProcessPool, those Session cookies will be passed back to the server for each request. If you change the process pool for a WKWebView, you are essentially removing the session cookies for all future requests.
Don't waste you time in extracting cookies from iOS 11 below device, there are very less chances of getting succeeded. Cookie extraction may get blocked due some security reasons.
Refer these logs:
2019-02-07 00:05:45.548880+0530 MyApp[2278:280725] [BoringSSL] nw_protocol_boringssl_get_output_frames(1301) [C8.1:2][0x10fd776f0] get output frames failed, state 8196
2019-02-07 00:05:45.550915+0530 MyApp[2278:280725] TIC Read Status [8:0x0]: 1:57
Try this code which is build for below iOS 11 devices:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
let cookieValue = HTTPCookieStorage.shared.cookies(for: navigationResponse.response.url!)
print(cookieValue!)
let response = navigationResponse.response as! HTTPURLResponse
let headFields = response.allHeaderFields as! [String:String]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headFields, for: response.url!)
for cookie in cookies {
print("name: \(cookie.name) value: \(cookie.value)")
}
decisionHandler(.allow)
}
The above code will give you empty cookie array, as cookies extraction are being blocked due to some security reasons.
I would recommend you to try following which is meant for iOS 11 and above:
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { (cookies) in
for cookie in cookies {
print(cookie)
}
}
In practice, I found in the method of "decidePolicyForNavigationResponse", you can use following way to fetch cookies, but the sad thing is it's not a complete/whole list for a session.
let response = navigationResponse.response as! NSHTTPURLResponse
let headFields = response.allHeaderFields as! [String:String]
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headFields, forURL: response.URL!)
In NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url), what happen if the url where the cookies are set is not a navigation response url (url that causes a navigation)? I notice the callback url where the cookies are set is never called in decidePolicyFor navigationResponse.
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
let response = navigationResponse.response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: response.allHeaderFields as! [String : String], for: response.url!)
}
The above delegate is never executed for the callback url since the callback itself does not caused a page navigation.
cookies(withResponseHeaderFields:for:)
Solved for iOS 11 or above, the only thing you need to do is write the assíncronos method to get all the cookies this way:
webView.configuration.processPool = [[WKProcessPool alloc] init];
[[WKWebsiteDataStore defaultDataStore].httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * cookies) {
//DO YOUR STUFF WITH THE 'cookies' VARIABLE
}
This post has useful information on cookie handling with WKWebView. According to this you should be able to set and retrieve cookies using the standard NSURLCache and NSHTTPCookie. He also refers to using WKProccessPool as per Stephan's comment.

how to Add timeout for WKWebview

How to write a timeout handler for WKWebView, when default delegates are not getting called for didFailNavigation.
WKWebView delegate are set & DidFinishNavigation or didFailProvisionalNavigation is getting called.
Use the error.code value of the error that didFailProvisionalNavigation creates and add your 'handler' code there:
func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) {
if error.code == -1001 { // TIMED OUT:
// CODE to handle TIMEOUT
} else if error.code == -1003 { // SERVER CANNOT BE FOUND
// CODE to handle SERVER not found
} else if error.code == -1100 { // URL NOT FOUND ON SERVER
// CODE to handle URL not found
}
}
Use this delegate method
webView:didFailProvisionalNavigation:withError:
Document
Invoked when an error occurs while starting to load data for the main frame.
And check the error code
NSURLErrorTimedOut = -1001
All the error code list
One possible solution is to add custom timer, which starts as you call loadHTML, loadRequest methods and times out on custom interval
Compared to Timer , asyncAfter(deadline:) is more light-weighted.
var isTimeOut = true
DispatchQueue.main.asyncAfter(deadline: .now() + timeOut) {
if isTimeOut{
// do time out thing
}
}
check isTimeOut according to WKNavigationDelegate
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
isTimeOut = false
}

Getting all cookies from WKWebView

while getting cookies from UIWebView seems straightforward by using NSHTTPCookieStorage.sharedHTTPCookieStorage(), it seems WKWebView stores the cookies somewhere else.
I did some research, and I was able to get some cookies from the grabbing it from NSHTTPURLResponse object. this, however, does not contain all the cookies used by WKWebView:
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let httpResponse = navigationResponse.response as? NSHTTPURLResponse {
if let headers = httpResponse.allHeaderFields as? [String: String], url = httpResponse.URL {
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url)
for cookie in cookies {
logDebug(cookie.description)
logDebug("found cookie " + cookie.name + " " + cookie.value)
}
}
}
}
Strangely, there's also a class WKWebsiteDataStore in ios 9 that responsible for managing cookies in WKWebView, however, the class does not contain a public method to retrieve the cookies data:
let storage = WKWebsiteDataStore.defaultDataStore()
storage.fetchDataRecordsOfTypes([WKWebsiteDataTypeCookies], completionHandler: { (records) -> Void in
for record in records {
logDebug("cookie record is " + record.debugDescription)
for dataType in record.dataTypes {
logDebug("data type is " + dataType.debugDescription)
// get cookie data??
}
}
})
Is there a workaround for getting the cookie data?
Cookies used (created) by the WKWebView are actually correctly stored in the NSHTTPCookieStorage.sharedHTTPCookieStorage().
The problem is that the WKWebView does not write back the cookies immediately. I think it does this on its own schedule. For example when a WKWebView is closed or maybe periodically.
So eventually they do end up in there, but when is unpredictable.
You may be able to force a 'sync' to the shared NSHTTPCookieStorage by closing your WKWebView. Please let us know if this works.
Update: I just remembered that in Firefox for iOS we force the WKWebView to flush its internal data, including cookies, by replacing its WKProcessPool with a new one. There is no official API, but I am pretty sure that is the most reliable workaround right now.
Finally, httpCookieStore for WKWebsiteDataStore landed in iOS 11.
https://developer.apple.com/documentation/webkit/wkwebsitedatastore?changes=latest_minor
Details
Xcode 9.2, Swift 4
Xcode 10.2 (10E125), Swift 5
Solution
extension WKWebView {
private var httpCookieStore: WKHTTPCookieStore { return WKWebsiteDataStore.default().httpCookieStore }
func getCookies(for domain: String? = nil, completion: #escaping ([String : Any])->()) {
var cookieDict = [String : AnyObject]()
httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
if let domain = domain {
if cookie.domain.contains(domain) {
cookieDict[cookie.name] = cookie.properties as AnyObject?
}
} else {
cookieDict[cookie.name] = cookie.properties as AnyObject?
}
}
completion(cookieDict)
}
}
}
Usage
// get cookies for domain
webView.getCookies(for: url.host) { data in
print("=========================================")
print("\(url.absoluteString)")
print(data)
}
// get all cookies
webView.getCookies() { data in
print("=========================================")
print("\(url.absoluteString)")
print(data)
}
Full sample
Info.plist
add in your Info.plist transport security setting
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Code
Do not forget to add the solution code here
ViewController has embed view controller
import UIKit
import WebKit
class ViewController: UIViewController {
private lazy var url = URL(string: "https://google.com")!
private weak var webView: WKWebView?
func initWebView(configuration: WKWebViewConfiguration) {
if webView != nil { return }
let webView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)
self.webView = webView
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if webView == nil { initWebView(configuration: WKWebViewConfiguration()) }
webView?.load(url: url)
}
}
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if let url = webView.url {
webView.getCookies(for: url.host) { data in
print("=========================================")
print("\(url.absoluteString)")
print(data)
}
}
}
}
extension ViewController: WKUIDelegate {
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
// push new screen to the navigation controller when need to open url in another "tab"
if let url = navigationAction.request.url, navigationAction.targetFrame == nil {
let viewController = ViewController()
viewController.initWebView(configuration: configuration)
viewController.url = url
DispatchQueue.main.async { [weak self] in
self?.navigationController?.pushViewController(viewController, animated: true)
}
return viewController.webView
}
return nil
}
}
extension WKWebView {
func load(urlString: String) {
if let url = URL(string: urlString) { load(url: url) }
}
func load(url: URL) { load(URLRequest(url: url)) }
}
I know this is a very old question, and we have a solution but work only on iOS 11 and upper. For those one who are dealing with iOS 10 and lower (like me), you may consider this method. It works perfectly to me:
Force reset processPool:
extension WKWebView {
func refreshCookies() {
self.configuration.processPool = WKProcessPool()
// TO DO: Save your cookies,...
}
}
--> this only work on real device.
For simulator, you should add:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
if let response = navigationResponse.response as? HTTPURLResponse,
let allHttpHeaders = response.allHeaderFields as? [String: String],
let responseUrl = response.url {
let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHttpHeaders, for: responseUrl)
for cookie in cookies {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
decisionHandler(.allow)
}
Follow to the answer of Stefan Arentz and Phenom.
For iOS 11, without any extensions:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
//...
}
}
}
if (#available(iOS 11.0, *)) {
[webView.configuration.websiteDataStore.httpCookieStore
getAllCookies:^(NSArray<NSHTTPCookie *> *_Nonnull cookies) {
NSURLRequest *request =
[[NSURLRequest alloc] initWithURL:self.URL]; //your URL
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session
dataTaskWithRequest:request
completionHandler:^(NSData *responseData, NSURLResponse *response,
NSError *error) {
//Do Something
}];
[task resume];
[session.configuration.HTTPCookieStorage storeCookies:cookies forTask:task];
}];
}
I used WKHTTPCookieStore in Objective-C, This worked for me to get both persistent and session cookies, but it only works in iOS 11+
https://developer.apple.com/documentation/webkit/wkhttpcookiestore?changes=latest_minor&language=objc
if (#available(iOS 11.0, *)) {
WKHTTPCookieStore *cookieStore = _webView.configuration.websiteDataStore.httpCookieStore;
[cookieStore getAllCookies:^(NSArray* cookies) {
NSHTTPCookie *cookie;
for(cookie in cookies){
NSLog(#"cookie: %#", cookie);
}
}];
Forcing the WKWebView to flush its internal data by replacing its WKProcessPool as described by Stefan's answer worked for me in iOS 10 and 11 but only for persistent cookies; it seems like session cookies get removed, as J. Thoo described
Swift 5
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
debugPrint(cookies.debugDescription)
}
decisionHandler(.allow)
}
As Stefan mentioned, cookies are stored in
NSHTTPCookieStorage.sharedHTTPCookieStorage()
However, from my experiments, I found that Session cookies set by the server are not visible to NSHTTPCookieStorage.sharedHTTPCookieStorage().
As long as each WKWebView share the same instance of WKProcessPool, those Session cookies will be passed back to the server for each request. If you change the process pool for a WKWebView, you are essentially removing the session cookies for all future requests.
Don't waste you time in extracting cookies from iOS 11 below device, there are very less chances of getting succeeded. Cookie extraction may get blocked due some security reasons.
Refer these logs:
2019-02-07 00:05:45.548880+0530 MyApp[2278:280725] [BoringSSL] nw_protocol_boringssl_get_output_frames(1301) [C8.1:2][0x10fd776f0] get output frames failed, state 8196
2019-02-07 00:05:45.550915+0530 MyApp[2278:280725] TIC Read Status [8:0x0]: 1:57
Try this code which is build for below iOS 11 devices:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
let cookieValue = HTTPCookieStorage.shared.cookies(for: navigationResponse.response.url!)
print(cookieValue!)
let response = navigationResponse.response as! HTTPURLResponse
let headFields = response.allHeaderFields as! [String:String]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headFields, for: response.url!)
for cookie in cookies {
print("name: \(cookie.name) value: \(cookie.value)")
}
decisionHandler(.allow)
}
The above code will give you empty cookie array, as cookies extraction are being blocked due to some security reasons.
I would recommend you to try following which is meant for iOS 11 and above:
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { (cookies) in
for cookie in cookies {
print(cookie)
}
}
In practice, I found in the method of "decidePolicyForNavigationResponse", you can use following way to fetch cookies, but the sad thing is it's not a complete/whole list for a session.
let response = navigationResponse.response as! NSHTTPURLResponse
let headFields = response.allHeaderFields as! [String:String]
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headFields, forURL: response.URL!)
In NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url), what happen if the url where the cookies are set is not a navigation response url (url that causes a navigation)? I notice the callback url where the cookies are set is never called in decidePolicyFor navigationResponse.
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
let response = navigationResponse.response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: response.allHeaderFields as! [String : String], for: response.url!)
}
The above delegate is never executed for the callback url since the callback itself does not caused a page navigation.
cookies(withResponseHeaderFields:for:)
Solved for iOS 11 or above, the only thing you need to do is write the assíncronos method to get all the cookies this way:
webView.configuration.processPool = [[WKProcessPool alloc] init];
[[WKWebsiteDataStore defaultDataStore].httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * cookies) {
//DO YOUR STUFF WITH THE 'cookies' VARIABLE
}
This post has useful information on cookie handling with WKWebView. According to this you should be able to set and retrieve cookies using the standard NSURLCache and NSHTTPCookie. He also refers to using WKProccessPool as per Stephan's comment.

Resources