web-dev-qa-db-ja.com

Swift iOSでオフラインビュー用のWKWebViewコンテンツをキャッシュする

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フレームワークを使用しようとしましたが、不完全でした。

enter image description here

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週間以上も苦労しており、それが私たちの主な機能です。どんなアイデアでも本当にありがたいです。

ありがとうございました!

15
Cristi Ghinea

私は遅れていることを知っていますが、最近、オフラインで読むために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 にのみ依存します。

7

App Cacheを使用する可能性を調査することをお勧めします。これは、iOS 10以降でWKWebViewでサポートされています。 https://stackoverflow.com/a/44333359/233602

2
Andrew Ebling

既にアクセスしたページをキャッシュするだけなのか、それともキャッシュしたい特定のリクエストがあるのか​​はわかりません。私は現在後者に取り組んでいます。だから私はそれに話します。私の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)
    }
}
_
0
FromTheStix

キャッシュ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)
    }
}
0
Sidd