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)
- 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)
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:
☑️ 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,
anddocument.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
Window.Scrollto Not Working in Phonegap - Alternative Solution or Workaround
How to Make Cross-Browser CSS3 Code Dry
Performance Difference Between JavaScript Created Inline Styles and JavaScript Created Stylesheets
Webkit Transitionend Event Grouping
How to Include CSS Files from an MVC Partial Control
Convert from English Digits to Arabic Ones in HTML Page
How to Dynamically Change the Style Tag Using JavaScript
Disable Inertia Scroll for "Single-Page" Webapp
How to Achieve a Consistent Layout in All Browsers
How to Measure the Space That a Text Will Take in JavaScript
How to Override Inline CSS Through JavaScript
Make Named Anchor Bookmarks Appear Always at Top of the Screen When Clicked
How to Click on Hidden Element in Protractor