2

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.

mehdok
  • 1,499
  • 4
  • 29
  • 54
  • What URL scheme do you use in the `MyProtocol` example? Also, from my experience, overriding `init` in the `URLProtocol` protocol subclass is not required. – Russian May 11 '20 at 15:55
  • It doesn't matter what scheme I'm using, it won't work for any scheme. As you can see in the code I used `localhost`, I can use any other custom scheme and it still doesn't work. – mehdok May 11 '20 at 16:07
  • oh, You said in `MyProtocol`. for that I used regular scheme, `http`,`https` – mehdok May 11 '20 at 16:08
  • 1
    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:)`. – Russian May 11 '20 at 23:22
  • @Russian, It seems that your way is working just for initial request, but it can not capture request sequence include `css`, `js`, `font`... like `URLCache` does – mehdok May 13 '20 at 04:42
  • What types of URLs are used for these resources (css, js, fonts) – absolute or relative? – Russian May 13 '20 at 06:21
  • They are relative like `../styles/style.css` and I managed to make them trigger `func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask)` by adding `localhost` at the beginning of them like: `localhost://styles/style.css`. But is there any way to not do that and still get that method triggred? – mehdok May 13 '20 at 06:24
  • Well, this is strange, because, for instance, when working with custom URL protocols, "child" resources get requested with the expected custom scheme. If you can somehow share the content you are trying to load, I could take a look at it later today. – Russian May 13 '20 at 06:41
  • I think it's because I used `loadHTMLString` like: `wk.loadHTMLString(str, baseURL: URL(string: "localhost"))`. With this and `localhost://styles/style.css` any font inside css file like `src: url(../Fonts/NormalAR.ttf);` trigger that `start` method with url like `localhost://Fonts/NormalAR.ttf` I will update the original question with a sample. – mehdok May 13 '20 at 07:17
  • @Russian I added more info to question. – mehdok May 13 '20 at 07:24
  • I managed to fix my problem based on your first comment. Please add an answer based on that and i will accept it. @Russian – mehdok May 16 '20 at 10:30
  • @medok Sorry, I was not able to investigate your problem, but I'm glad you have figured it out yourself. The bounty has ended, but anyway I don't think my answer was worth that much of a reputation :) – Russian May 18 '20 at 21:12

3 Answers3

1

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
}())
Russian
  • 1,296
  • 10
  • 15
0

For what I understood since iOS 10 WKWebview support AppCache

try this code

let webview: WKWebView
let config = WKWebViewConfiguration()
config.setValue(true, forKey: "_setOfflineApplicationCacheIsEnabled")
webview = WKWebView(frame: .zero, configuration: config)
Erez Mizrahi
  • 197
  • 6
  • This throw an exception: `Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key _setOfflineApplicationCacheIsEnabled.'` Beside that, I think it's a hidden api and there is a good chance that apple reject the app because of that. – mehdok May 13 '20 at 04:46
  • Using non-public API's will cause the app to be rejected by Apple. – elliott-io May 16 '20 at 04:31
  • I see good to know. I hope some one will share the correct answer – Erez Mizrahi May 17 '20 at 08:56
0

WKNavigationDelegate has a function to decide how to navigate to a page, you could do filtering and redirects here. Something like this:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    // you may need to use .linkActivated depending on how users are navigating
    if navigationAction.navigationType == .other  {
        if let request = navigationAction.request.url {
            if request.absoluteString.hasPrefix("https://somePrefix") {
                // for test
               // guard let localRedirect = URL.init(string: "https://google.com") else {
                guard let localRedirect = URL.init(string: "localhost://someUrl") else {
                    decisionHandler(.allow)
                    return
                }
                decisionHandler(.cancel)
                // for test
                // webView.load(URLRequest.init(url: URL.init(string: "https://www.google.com")!))
                webView.loadFileURL(localRedirect, allowingReadAccessTo: localRedirect.deletingLastPathComponent()) // delete last path component if you have other things that the page will load in that directory, like images or css
                return
            }
        }
    }
    decisionHandler(.allow)
}

I've commented out navigating to google as a test, but left the code there so people can see easily how a redirect would work.

elliott-io
  • 1,394
  • 6
  • 9
  • If I misunderstood your objective, please help clarify what your are trying to do. – elliott-io May 17 '20 at 22:58
  • I'm not seeking to just see the requests and redirect them to some other url. I'm trying to intercept the requests and replace them with local content. Please see EDIT 1, I have shared some code that shows what I'm trying to do. (codes in edit 1 is working with one minor downside.) Please see my latest comments with @Russain on the original question. – mehdok May 18 '20 at 07:47