How to Intercept a Wkwebview Request to Detect Which Local Resource Files (Css, Js, Png, ...) Load Together with a HTML File

Load local web files & resources in WKWebView

Updated for Swift 4, Xcode 9.3


This methods allows WKWebView to properly read your hierarchy of directories and sub-directories for linked CSS, JS and most other files. You do NOT need to change your HTML, CSS or JS code.

Solution (Quick)

  1. Add the web folder to your project (File > Add Files to Project)

    • Copy items if needed
    • Create folder references *
    • Add to targets (that are applicable)
  2. Add the following code to the viewDidLoad and personalize it to your needs:

    let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "website")!
    webView.loadFileURL(url, allowingReadAccessTo: url)
    let request = URLRequest(url: url)
    webView.load(request)

Solution (In-Depth)

Step 1

Import the folder of local web files anywhere into your project. Make sure that you:

Xcode > File > Add Files to "Project"

☑️ Copy items if needed

☑️ Create folder references (not "Create groups")

☑️ Add to targets

Step 2

Go to the View Controller with the WKWebView and add the following code to the viewDidLoad method:

let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "website")!
webView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
webView.load(request)
  • index – the name of the file to load (without the .html extension)
  • website – the name of your web folder (index.html should be at the root of this directory)

Conclusion

The overall code should look something like this:

import UIKit
import WebKit

class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {

@IBOutlet weak var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

webView.uiDelegate = self
webView.navigationDelegate = self

let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "Website")!
webView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
webView.load(request)
}

}

If any of you have further questions about this method or the code, I'll do my best to answer!

Intercept request with WKWebView

I see that after 5 years this question still generates curiosity, so I describe how I solved it and about some main problems I faced up.
As many who answered here, I have implemented WKURLSchemeHandler and used new schemes.

First of all the URL that wkwebview launches must not be HTTP (or HTTPS) but one of yours new schemes.

Example

mynewscheme://your-server-application.com

In you WKWebViewConfiguration conf, I set the handler:

[conf setURLSchemeHandler:[CustomSchemeHandler new] forURLScheme:@"mynewscheme"];
[conf setURLSchemeHandler:[CustomSchemeHandler new] forURLScheme:@"mynewschemesecure"];

In CustomSchemeHandler I have implemented webView:startURLSchemeTask: and webView:stopURLSchemeTask:.

In my case I check if the request is for a file that I just saved locally, otherwise I change actual protocol ("mynewscheme or "mynewschemesecure") with http (or https) and I make request by myself.

At this point I solved the "interception problem".

In this new way we have the webview "location" (location.href via javascript) with my new scheme and with it new problems started.

  • First problem is that my applications work mainly with javascript,
    and document.cookie has stopped working. I'm using Cordova
    framework, so I've develeped a plugin to set and get cookie to
    replace document.cookie (I had to do this, because, obviously, I
    have also http header set-cookie).

  • Second problem is that I've got a lot of "cross-origin" problems, then
    I changed all my urls in relative url (or with new schemes)

  • Third problem is that browser automatically handle server port 80
    and 443, omitting them, but has now stopped (maybe because of "not
    http location"). In my server code I had to handle this.

Writing down these few rows I admit that it seems to was an easy problem to solve, but I ensure that find out a workaround, how to solve it and integrate with the infinite amount of code has been hard. Every step towards the solution corresponded to a new problem.

How to enable WKURLSchemeHandler to do work off main thread?

I finally figured it out. I can't believe how difficult this was. No wonder Apple hasn't released any samples around this. Here's my code:

// This is based on "Customized Loading in WKWebView" WWDC video (near the end of the
// video) at https://developer.apple.com/videos/play/wwdc2017/220 and A LOT of trial
// and error to figure out how to push work to background thread.
//
// To better understand how WKURLSchemeTask (and internally WebURLSchemeTask) works
// you can refer to the source code of WebURLSchemeTask at
// https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/WebURLSchemeTask.cpp
//
// Looking at that source code you can see that a call to any of the internals of
// WebURLSchemeTask (which is made through WKURLSchemeTask) is expected to be on the
// main thread, as you can see by the ASSERT(RunLoop::isMain()) statements at the
// beginning of pretty much every function and property getters. I'm not sure why Apple
// has decided to do these on the main thread since that would result in a blocked UI
// thread if we need to return large responses/files. At the very least they should have
// allowed for calls to come back on any thread and internally pass them to the main
// thread so that developers wouldn't have to write thread-synchronization code over and
// over every time they want to use WKURLSchemeHandler.
//
// The solution to pushing things off main thread is rather cumbersome. We need to call
// into DispatchQueue.global(qos: .background).async {...} but also manually ensure that
// everything is synchronized between the main and bg thread. We also manually need to
// keep track of the stopped tasks b/c a WKURLSchemeTask does not have any properties that
// we could query to see if it has stopped. If we respond to a WKURLSchemeTask that has
// stopped then an unmanaged exception is thrown which Swift cannot catch and the entire
// app will crash.
public class MyURLSchemeHandler: NSObject, WKURLSchemeHandler {
private var stoppedTaskURLs: [URLRequest] = []

public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let request = urlSchemeTask.request
guard let requestUrl = request.url else { return }

DispatchQueue.global(qos: .background).async { [weak self] in
guard let strongSelf = self, requestUrl.scheme == "my-video-url-scheme" else {
return
}

let filePath = requestUrl.absoluteString
if let fileHandle = FileHandle(forReadingAtPath: filePath) {
// video files can be very large in size, so read them in chuncks.
let chunkSize = 1024 * 1024 // 1Mb
let response = URLResponse(url: requestUrl,
mimeType: "video/mp4",
expectedContentLength: chunkSize,
textEncodingName: nil)
strongSelf.postResponse(to: urlSchemeTask, response: response)
var data = fileHandle.readData(ofLength: chunkSize) // get the first chunk
while (!data.isEmpty && !strongSelf.hasTaskStopped(urlSchemeTask)) {
strongSelf.postResponse(to: urlSchemeTask, data: data)
data = fileHandle.readData(ofLength: chunkSize) // get the next chunk
}
fileHandle.closeFile()
strongSelf.postFinished(to: urlSchemeTask)
} else {
strongSelf.postFailed(
to: urlSchemeTask,
error: NSError(domain: "Failed to fetch resource",
code: 0,
userInfo: nil))
}

// remove the task from the list of stopped tasks (if it is there)
// since we're done with it anyway
strongSelf.stoppedTaskURLs = strongSelf.stoppedTaskURLs.filter{$0 != request}
}
}

public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
if (!self.hasTaskStopped(urlSchemeTask)) {
self.stoppedTaskURLs.append(urlSchemeTask.request)
}
}

private func hasTaskStopped(_ urlSchemeTask: WKURLSchemeTask) -> Bool {
return self.stoppedTaskURLs.contains{$0 == urlSchemeTask.request}
}

private func postResponse(to urlSchemeTask: WKURLSchemeTask, response: URLResponse) {
post(to: urlSchemeTask, action: {urlSchemeTask.didReceive(response)})
}

private func postResponse(to urlSchemeTask: WKURLSchemeTask, data: Data) {
post(to: urlSchemeTask, action: {urlSchemeTask.didReceive(data)})
}

private func postFinished(to urlSchemeTask: WKURLSchemeTask) {
post(to: urlSchemeTask, action: {urlSchemeTask.didFinish()})
}

private func postFailed(to urlSchemeTask: WKURLSchemeTask, error: NSError) {
post(to: urlSchemeTask, action: {urlSchemeTask.didFailWithError(error)})
}

private func post(to urlSchemeTask: WKURLSchemeTask, action: @escaping () -> Void) {
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async { [weak self] in
if (self?.hasTaskStopped(urlSchemeTask) == false) {
action()
}
group.leave()
}
group.wait()
}
}



Related Topics



Leave a reply



Submit