WKWebViewのコンテンツ(HTML)を永続ストレージ(NSUserDefaults、CoreDataまたはディスクファイル)に保存しようとしています。ユーザーは、インターネットに接続せずにアプリケーションに再び入ると、同じコンテンツを見ることができます。 WKWebViewは、UIWebViewのようなNSURLProtocolを使用しません(投稿 here を参照)。
「オフラインアプリケーションキャッシュがWKWebViewで有効になっていません」という投稿を見たことはありますが。 (アップル開発フォーラム)、私は解決策が存在することを知っています。
私は2つの可能性について学びましたが、それらを機能させることができませんでした。
1)Safari for MacでWebサイトを開いて[ファイル] >> [名前を付けて保存]を選択すると、下の画像に次のオプションが表示されます。 Macアプリには[[[webView mainFrame] dataSource] webArchive]が存在しますが、UIWebViewまたはWKWebViewにはそのようなAPIはありません。しかし、WKWebViewのXcodeに.webarchiveファイル(Mac Safariから取得したファイルなど)をロードすると、インターネット接続がない場合、コンテンツは正しく表示されます(html、外部画像、ビデオプレビュー)。 .webarchiveファイルは、実際にはplist(プロパティリスト)です。 .webarchiveファイルを作成するMacフレームワークを使用しようとしましたが、不完全でした。
2)webView:didFinishNavigationでHTMLを廃止しましたが、外部画像、CSS、JavaScriptが保存されません
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()",
completionHandler: { (html: AnyObject?, error: NSError?) in
print(html)
})
}
私たちは1週間以上も苦労しており、それが私たちの主な機能です。どんなアイデアでも本当にありがたいです。
ありがとうございました!
私は遅れていることを知っていますが、最近、オフラインで読むためにWebページを保存する方法を探していましたが、ページ自体に依存せず、非推奨の_を使用しない信頼できる解決策を見つけることができませんでした[$ var] _。多くの人が既存のHTTPキャッシングを使用すべきだと書いていますが、WebKitはアウトプロセスで多くのことを行うようで、完全なキャッシングを強制することは事実上不可能になっています( here または-を参照)。 ここ )。しかし、この質問は私を正しい方向に導きました。 Webアーカイブアプローチをいじくり回すと、実際に独自のWebアーカイブエクスポーターを作成するのは非常に簡単です。
質問に書かれているように、Webアーカイブは単なるplistファイルなので、必要なのはHTMLページから必要なリソースを抽出し、それらをすべてダウンロードして大きなplistファイルに保存するクローラーだけです。このアーカイブファイルは、後でloadFileURL(URL:allowingReadAccessTo:)
を介してUIWebView
にロードできます。
このアプローチを使用してWKWebView
からのアーカイブとWKWebView
への復元を可能にするデモアプリを作成しました: https://github.com/ernesto-elsaesser/OfflineWebView
実装は、HTML解析の Fuzi にのみ依存します。
App Cacheを使用する可能性を調査することをお勧めします。これは、iOS 10以降でWKWebView
でサポートされています。 https://stackoverflow.com/a/44333359/233602
既にアクセスしたページをキャッシュするだけなのか、それともキャッシュしたい特定のリクエストがあるのかはわかりません。私は現在後者に取り組んでいます。だから私はそれに話します。私のURLはAPIリクエストから動的に生成されます。この応答から、画像以外のURLをrequestPaths
に設定し、各URLにリクエストを送信して、応答をキャッシュします。画像のURLについては、Kingfisherライブラリを使用して画像をキャッシュしました。 AppDelegateで共有キャッシュ_urlCache = URLCache.shared
_をすでに設定しています。そして、必要なメモリを割り当てます:urlCache = URLCache(memoryCapacity: <setForYourNeeds>, diskCapacity: <setForYourNeeds>, diskPath: "urlCache")
次に、requestPaths
の各URLに対してstartRequest(:_)
を呼び出します。 (すぐに必要なければバックグラウンドで実行できます)
_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)")
}
})
}
}
}
}
_
私は適切なヘッダーで構成されたcachingSessionManagerでネットワークリクエストにAlamofireを使用しています。だから私の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でWebビューをロードします。 (ふ!!)かなりのプロセスだったし、たぶんもっと簡単な方法があるかもしれないけど……。私のコードは、これらすべての変更を反映するように更新されています。幸運を!
_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)
}
}
_
キャッシュWebページを使用する最も簡単な方法は、次のようになりますSwift 4.:-
/ * 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)
}
}