我们正在尝试将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框架,但它不完整。
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)
})
}
我们挣扎了一个星期,这对我们来说是一个主要特征。 任何想法都非常感激。
谢谢!
答案 0 :(得分:5)
我知道我来晚了,但是最近我一直在寻找一种存储网页以供离线阅读的方法,但仍然找不到任何不依赖于页面本身并且不会使用的可靠解决方案不推荐使用的UIWebView
。许多人写道,应该使用现有的HTTP缓存,但是WebKit似乎在进程外做很多事情,因此实际上不可能实施完整的缓存(请参阅here或here )。但是,这个问题将我引向正确的方向。修补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)
}
}