Swift iOS Cache WKWebView contenido para vista sin connection

Intentamos save el contenido (HTML) de WKWebView en un almacenamiento persistente (NSUserDefaults, CoreData o file de disco). El usuario puede ver el mismo contenido cuando vuelve a ingresar a la aplicación sin connection a internet. WKWebView no utiliza NSURLProtocol como UIWebView (ver publicación aquí ).

Aunque he visto publicaciones que "El caching de aplicaciones sin connection no está habilitado en WKWebView". (Foros de Apple dev), sé que existe una solución.

He aprendido dos posibilidades, pero no pude hacer que funcionen:

1) Si abro un website en Safari para Mac y selecciono Archivo >> Guardar como, aparecerá la siguiente opción en la image a continuación. Para las aplicaciones de Mac existe [[[webView mainFrame] dataSource] webArchive], pero en UIWebView o WKWebView no existe dicha API. Pero si carga un file .webarchive en Xcode en WKWebView (como el que obtuve de Mac Safari), el contenido se muestra correctamente (html, imágenes externas, vistas previas de video) si no hay connection a Internet. El file .webarchive es en realidad un plist (list de properties). Intenté usar un marco mac que crea un file .webarchive, pero estaba incompleto.

introduzca la descripción de la imagen aquí

2) Obtuve el HTML en webView: didFinishNavigation pero no guarda imágenes externas, CSS, javascript

func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) { webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: { (html: AnyObject?, error: NSError?) in print(html) }) } 

Estamos luchando durante una semana y es una característica principal para nosotros. Cualquier idea es realmente apreciada.

¡Gracias!

Recomendaría investigar la viabilidad de usar App Cache, que ahora es compatible con WKWebView partir de iOS 10: https://stackoverflow.com/a/44333359/233602

No estoy seguro si solo desea almacenar en caching las páginas que ya se visitaron o si tiene requestes específicas que le gustaría almacenar en caching. Actualmente estoy trabajando en este último. Entonces hablaré de eso. Mis URL se generan dinámicamente a partir de una request de API. A partir de esta respuesta, establezco requestPaths con las direcciones URL no de image y luego hago una request para cada una de las direcciones URL y cacheo la respuesta. Para las URL de image, utilicé la biblioteca Kingfisher para almacenar en caching las imágenes. Ya he configurado mi caching compartida urlCache = URLCache.shanetworking en mi AppDelegate. Y asigné la memory que necesito: urlCache = URLCache(memoryCapacity: <setForYourNeeds>, diskCapacity: <setForYourNeeds>, diskPath: "urlCache") Luego simplemente llama startRequest(:_) para cada una de las URL en requestPaths . (Se puede hacer en segundo plano si no se necesita de inmediato)

 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)") } }) } } } } 

Estoy usando Alamofire para la request de networking con un cachingSessionManager configurado con los encabezados adecuados. Entonces, en mi class WebService, tengo:

 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 } 

Luego, en el método del delegado de webview, cargué el cachedResponse. Utilizo una variable handlingCacheRequest para evitar un bucle infinito.

 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) } 

Por supuesto que querrás manejarlo si también hay un error de carga.

 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() } 

Espero que esto ayude. Lo único que sigo intentando descubrir es que los elementos de image no se cargan sin connection. Estoy pensando que necesitaré hacer una request por separado para esas imágenes y mantener una reference local. Solo un pensamiento, pero actualizaré esto cuando haya funcionado.

ACTUALIZADO con las imágenes cargando sin connection con el código siguiente Utilicé la biblioteca Kanna para analizar mi cadena html de mi respuesta almacenada en caching, encuentra la url incrustada en el style= background-image: atributo de la div, regex usado para get la url (que también la key para la image en caching de Kingfisher), buscó la image almacenada en caching y luego modificó el CSS para usar los datos de la image (en base a este artículo: https://css-tricks.com/data-uris/ ), y luego cargó la vista web con el html modificado. (¡Uf!) Fue todo el process y tal vez hay una manera más fácil … pero no lo había encontrado. Mi código se actualiza para reflejar todos estos cambios. ¡Buena suerte!

 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) } }