用于离线视图的Swift iOS Cache WKWebView内容

时间:2016-03-27 10:49:27

标签: ios swift caching offline wkwebview

我们正在尝试将WKWebView的内容(HTML)保存在永久存储(NSUserDefaults,CoreData或磁盘文件)中。用户在没有互联网连接的情况下重新进入应用程序时可以看到相同的内容。 WKWebView不像UIWebView那样使用NSURLProtocol(参见帖子here)。

虽然我看过帖子" WKWebView中没有启用离线应用程序缓存。" (Apple dev论坛),我知道存在一个解决方案。

我已经了解了两种可能性,但我无法使它们发挥作用:

1)如果我在Safari for Mac中打开一个网站,请选择文件>>另存为,它将在下图中显示以下选项。对于Mac应用程序存在[[[webView mainFrame] dataSource] webArchive],但在UIWebView或WKWebView上没有这样的API。但是如果我在WKWebView上的Xcode中加载.webarchive文件(就像我从Mac Safari获得的那样),那么如果没有互联网连接,内容会正确显示(html,外部图像,视频预览)。 .webarchive文件实际上是一个plist(属性列表)。我试图使用一个创建.webarchive文件的mac框架,但它不完整。

enter image description here

2)我在webView中删除了HTML:didFinishNavigation但它没有保存外部图像,css,javascript

 func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {

    webView.evaluateJavaScript("document.documentElement.outerHTML.toString()",
        completionHandler: { (html: AnyObject?, error: NSError?) in
            print(html)
    })
}

我们挣扎了一个星期,这对我们来说是一个主要特征。 任何想法都非常感激。

谢谢!

4 个答案:

答案 0 :(得分:5)

我知道我来晚了,但是最近我一直在寻找一种存储网页以供离线阅读的方法,但仍然找不到任何不依赖于页面本身并且不会使用的可靠解决方案不推荐使用的UIWebView。许多人写道,应该使用现有的HTTP缓存,但是WebKit似乎在进程外做很多事情,因此实际上不可能实施完整的缓存(请参阅herehere )。但是,这个问题将我引向正确的方向。修补Web存档方法后,我发现编写自己的Web存档导出器实际上很容易。

正如问题中所写,Web存档只是plist文件,因此只需要一个爬网程序即可从HTML页面提取所需资源,将所有资源下载并存储在大plist文件中。然后,可以稍后通过WKWebView将这个存档文件加载到loadFileURL(URL:allowingReadAccessTo:)中。

我创建了一个演示应用程序,该应用程序允许使用以下方法从WKWebView进行存储和恢复:https://github.com/ernesto-elsaesser/OfflineWebView

对于XPath查询,实现仅取决于Fuzi。存档者的灵感来自BiblioArchiver (不幸的是不再编译)。

答案 1 :(得分:1)

我建议调查使用App Cache的可行性,现在iOS {10}中的WKWebView支持该缓存:https://stackoverflow.com/a/44333359/233602

答案 2 :(得分:0)

我不确定您是否只想缓存已访问过的网页,或者您是否有特定要求缓存的请求。我目前正在研究后者。所以我会这样说。我的网址是根据api请求动态生成的。根据此响应,我使用非图像URL设置requestPaths,然后请求每个URL并缓存响应。对于图片网址,我使用 Kingfisher 库来缓存图片。我已经在AppDelegate中设置了共享缓存urlCache = URLCache.shared。并分配了我需要的内存:urlCache = URLCache(memoryCapacity: <setForYourNeeds>, diskCapacity: <setForYourNeeds>, diskPath: "urlCache")然后只需为startRequest(:_)中的每个网址调用requestPaths即可。 (如果不需要马上就可以在后台完成)

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

            })
        }
    }
}
}

我使用Alamofire进行网络请求,并使用适当的标头配置cachingSessionManager。所以在我的WebService类中我有:

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
}

然后在webview委托方法中加载cachedResponse。我使用变量handlingCacheRequest来避免无限循环。

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

当然,如果出现加载错误,您也会想要处理它。

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

我希望这会有所帮助。我唯一想知道的是图像资产没有被脱机加载。我以为我需要单独请求这些图片,并在本地保留对它们的引用。只是一个想法,但是当我解决这个问题时,我会更新它。

使用以下代码离线加载图片更新 我使用 Kanna 库从我的缓存响应中解析我的html字符串,找到嵌入在div的style= background-image:属性中的url,使用正则表达式来获取url(这也是关键)对于Kingfisher缓存图像),获取缓存的图像,然后修改css以使用图像数据(基于本文:https://css-tricks.com/data-uris/),然后使用修改后的html加载webview。 (P!)这是一个很好的过程,也许还有一个更简单的方法......但我没有找到它。我的代码已更新,以反映所有这些更改。祝你好运!

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

答案 3 :(得分:0)

使用缓存网页的最简单方法如下 Swift 4.0 :-

/ *其中isCacheLoad = true(离线加载数据)&     isCacheLoad = false(正常加载数据)* /

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