Search code examples
iosswiftwkwebviewnsurlcacheurlcache

WKWebView doesn't use URLCache


I have an application which use UIWebView to show some local content. I Also have a custom URLCache to intercept requests and replace them with local content:

class LocalWebViewCache: URLCache {
    open override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
    if url.absoluteString.range(of: "http://localhost") != nil {
            return cacheDelegate?.handleRequest(request)
        }

        return nil
    }
}

And I registered this cache in AppDelagate as:

var cache: LocalWebViewCache = LocalWebViewCache(memoryCapacity: 20 * 1024 * 1024,
                                         diskCapacity: 100 * 1024 * 1024,
                                         diskPath: nil)
URLCache.shared = cache

Right now everything is working fine. BUT apple rejected the new version of app because I'm using UIWebView and it's deprecated, So I replaced the UIWebView with WKWebView and it seems that webkit doesn't honor the shared cache (URLCache.shared).

I tried to intercept WKWebView requests with URLProtocol and WKURLSchemeHandler without any luck.

I would appreciate any help or suggestion.

URLProtocol code:

in Appdelagate:

URLProtocol.registerClass(MyProtocol.self)
class MyProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        print("canInit: \(String(describing: request.url))")
        return true
    }

    override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
        print("init: \(String(describing: request.url))")

        super.init(request: request, cachedResponse: cachedResponse, client: client)
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        print("canonicalRequest: \(String(describing: request.url))")

        return request
    }

    override func startLoading() {
        print("startLoading: \(String(describing: request.url))")
    }
}

Among all this methods just canInit(with request: URLRequest) is getting called which is not very helpful.

WKURLSchemeHandler code:

class MySchemeHandler: NSObject, WKURLSchemeHandler {
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        print("start: \(String(describing: urlSchemeTask.request.url?.absoluteString))")
    }

    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
        print("stop: \(String(describing: urlSchemeTask.request.url?.absoluteString))")
    }
}
webkit.configuration.setURLSchemeHandler(MySchemeHandler(), forURLScheme: "localhost")

webkit.load(URLRequest(url: URL(string: "localhost://google.com")!))

But neither webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) nor webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) is getting called.


EDIT 1: Loading local resource by attaching localhost:// at the beginning of their url and handle it via WKURLSchemeHandler:

let testStr = """
            <!DOCTYPE html>
            <html>
            <head>
            <title></title>
            <link href="localhost://../styles/style.css" type="text/css" rel="stylesheet"/>
            </head>
            <body>

            <p class="test_class1">This is a paragraph.</p>
            <p class ="test_class2">This is another paragraph.</p>

            </body>
            </html>
    """
lazy var webView: WKWebView = {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration
            .setURLSchemeHandler(MySchemeHandler(), forURLScheme: "localhost")
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.translatesAutoresizingMaskIntoConstraints = false
        return webView
    }()
webView.loadHTMLString(testStr, baseURL: URL(string: "localhost"))
class MySchemeHandler: NSObject, WKURLSchemeHandler {
    let testCss = """
            @font-face{
                font-family: 'Special';
                font-weight: normal;
                font-style: normal;
                src: url(../fonts/special.ttf);
            }

            .test_class1 {
                font-weight: bold;
                color: #007D6E;
                font-family: 'Special' !important;
                text-align: center !important;
            }

            .test_class2 {
                font-weight: bold;
                color: #FF7D6E;
                text-align: center !important;
            }
    """

    func webView(_: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        print("start: \(String(describing: urlSchemeTask.request.url?.absoluteString))")

        guard let url = urlSchemeTask.request.url else {
            return
        }

        if url.absoluteString.contains("style.css") {
            let data = Data(testCss.utf8)

            urlSchemeTask.didReceive(URLResponse(url: url,
                                                 mimeType: "text/css",
                                                 expectedContentLength: data
                                                     .count,
                                                 textEncodingName: nil))
            urlSchemeTask.didReceive(data)
            urlSchemeTask.didFinish()
        }
    }

    func webView(_: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
        print("stop: \(String(describing: urlSchemeTask.request.url?.absoluteString))")
    }
}

In the log I can see that start method get triggered for local resources:

start: Optional("localhost://../styles/style.css")
start: Optional("localhost://../fonts/special.ttf")

But if I remove localhost:// from <link href="localhost://../styles/style.css" type="text/css" rel="stylesheet"/> the start method won't getting triggered at all.


Solution

  • Setting a handler on the result of calling WKWebView.configuration has no effect, since the copy is being returned. Instead, you should create a new instance of WKWebViewConfiguration, set your handler on it and then pass it to WKWebView.init(frame:, configuration:):

    self.schemeHandler = MySchemeHandler()
    self.webview = WKWebView(frame: .zero, configuration: {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.setURLSchemeHandler(self.schemeHandler, 
                                             forURLScheme: "localhost")
        return webConfiguration
    }())