40

In my Swift iOS app, I want to download some dynamic HTML pages from a remote server, save them in the document directory, and display those pages from document directory.

I was using this to load the page:

var appWebView:WKWebView?
...
appWebView!.loadRequest(NSURLRequest(URL: NSURL(fileURLWithPath: htmlPath)))

Everything works on the simulator, but when I moved to real phones, it just showed a blank page. I then connected to the app using Safari, and found it complained with "Failed to load resource".

I then tried to first read the content of the page at htmlPath, then use

appWebView!.loadHTMLString()

to load the page. It works when the HTML page is simple. But if the HTML references something else, i.e. a JavaScript file also in the document directory (with an absolute path like <script src="file:////var/mobile/Containers/Data/Application/762035C9-2BF2-4CDD-B5B1-574A0E2B0728/Documents/xxxxx.js">), it will fail to load.

Does anyone know why this happens, and how to resolve the issue?

More info:

  • XCode version: 7.3.1
  • Deployment Target: 8.1 (I tried to use 9.3 too, but that didn't help.)
Darshan Rivka Whittle
  • 32,989
  • 7
  • 91
  • 109
Peter
  • 775
  • 1
  • 6
  • 12
  • Did you check [this](http://stackoverflow.com/a/28676439/1044073)? – Roman Ermolov Sep 09 '16 at 22:51
  • Thank you!! I will try that.. – Peter Sep 09 '16 at 23:19
  • Please paste your download / saving html content code – Jakub Sep 12 '16 at 13:26
  • From my experience - WKWebView has loading issues when the webview is detached from the view hierarchy. This could be your issue. Also, you could try to following API for file requests: `func loadFileURL(_ URL: URL, allowingReadAccessTo readAccessURL: URL) -> WKNavigation?` [https://developer.apple.com/reference/webkit/wkwebview/1414973-loadfileurl] – DocForNoc Sep 20 '16 at 12:31

8 Answers8

42

This is a simplified version of what I have used to load local files in a project of mine (iOS 10, Swift 3). I have just updated my code (7.5.2017) after testing it out again on iOS 10.3.1 and iPhone 7+ as requested by Raghuram and Fox5150 in the comments.

I just created a completely new project and this is the folder structure:enter image description here

Update 19.04.2018: Added a new feature to download a .zip with HTML, CSS, JS files, unzip it in /Documents/ (Alamofire + Zip) and then load those files into the webView. You can find it in the GitHub sample project as well. Again, feel free to fork & star! :)

Update 08.02.2018: finally added a GitHub sample project, which also includes a local JavaScript file. Feel free to fork & star! :)

Version 1 with webView.loadFileURL()

ViewController.swift

import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        let webView = WKWebView()
        let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
        let htmlUrl = URL(fileURLWithPath: htmlPath!, isDirectory: false)
        webView.loadFileURL(htmlUrl, allowingReadAccessTo: htmlUrl)
        webView.navigationDelegate = self
        view = webView
    }
}

Version 2 with webView.loadHTMLString()

ViewController.swift

import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        let webView = WKWebView()
        let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
        let folderPath = Bundle.main.bundlePath
        let baseUrl = URL(fileURLWithPath: folderPath, isDirectory: true)
        do {
            let htmlString = try NSString(contentsOfFile: htmlPath!, encoding: String.Encoding.utf8.rawValue)
             webView.loadHTMLString(htmlString as String, baseURL: baseUrl)
        } catch {
            // catch error
        }
        webView.navigationDelegate = self
        view = webView
    }
}

Gotchas to look out for:

  • Make sure that your local html/js/css files are in Project -> Target -> Build Phases -> Copy Bundle Resources
  • Make sure that your html files don't reference relative paths e.g. css/styles.css because iOS will flatten your file structure and styles.css will be on the same level as index.html so write <link rel="stylesheet" type="text/css" href="styles.css"> instead

Given the 2 versions and the gotchas here are my html/css files from the project:

web/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    <title>Offline WebKit</title>
    <link rel="stylesheet" type="text/css" href="styles.css">
  </head>
  <body>
    <h1 id="webkit-h1">Offline WebKit!</h1>
  </body>
</html>

web/css/styles.css

#webkit-h1 {
  font-size: 80px;
  color: lightblue;
}

If somebody wants a GitHub sample project, tell me in the comments section and I'll upload it.

Cœur
  • 37,241
  • 25
  • 195
  • 267
tech4242
  • 2,348
  • 2
  • 23
  • 33
  • 1
    So looks like i might missed the "baseURL" argument when calling loadHTMLString. But I like what Roman pointed out better: (http://stackoverflow.com/questions/24882834/wkwebview-not-loading-local-files-under-ios-8/28676439#28676439). I actually do have a tag in my html page, which is referencing a remote server (as the file contain some javascript calling the remote server but was not using absolute path, and I don't want to go over the whole javascript file to update all those calls), at the same time, the html also are using some javascript and css files on the local drive. – Peter Sep 16 '16 at 12:12
  • I am getting this error: unexpectedly found nil while unwrapping an Optional value – Raghuram Apr 12 '17 at 10:31
  • @Raghuram can you give me some details - which iOS version etc.? I'll try to reproduce it when I have some spare time – tech4242 Apr 12 '17 at 14:16
  • @tech4242 xcode version 8.2.1 and device version 10.2.1 – Raghuram Apr 13 '17 at 04:48
  • @Raghuram thanks! and where exactly are you getting the nil? I am guessing `path!` ? – tech4242 Apr 13 '17 at 06:08
  • @tech4242 in the path – Raghuram Apr 13 '17 at 06:12
  • @Raghuram Ok, I'll have a look as soon as I can – tech4242 Apr 13 '17 at 13:54
  • 4
    @tech4242 Great answer! You can also support sub-folder references in your html / css / js, by adding your assets using folder references. You can just drag your html assets folder in your project structure in Xcode and select **"Create folder references"**. You then have access to your entire assets directory structure by doing the following: (let's say your folder is called **HtmlAssets**) `Bundle.main.url(forResource: "HtmlAssets", withExtension: nil)`. – m_katsifarakis Oct 16 '17 at 16:55
  • @m_katsifarakis thanks! Have to try that out some time soon :) – tech4242 Oct 16 '17 at 20:33
  • Thanks to @tech4242 and @m_katsifarakis I used this in Objective-C: `NSString* htmlPath = [NSBundle.mainBundle pathForResource:@"index" ofType:@"html" inDirectory:@"www"]; NSURL* baseUrl = [NSBundle.mainBundle URLForResource:@"www" withExtension:nil]; NSString* indexHTML = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];` where `index` and `www` should reflect your HTML file name and root folder of your web content, respectively. – Snouto Nov 10 '17 at 08:37
  • 3
    Version 1 is working with WKWebView on macOS. Version 2 doesn't work for me with WKWebView on macOS though. Seems that the allowingReadAccessTo param may be the key to unlocking access to the application on macOS to the baseURL path. – mservidio Nov 12 '17 at 00:36
  • @mservidio never really tried macOS tbh :) I really want to know how it works now though :) I'll try to carve out some time from work to have a look soon(ish) – tech4242 Nov 13 '17 at 09:56
  • @mservidio I've been troubleshooting this for days on macOS. WKWebView wasn't picking up local stylesheets and I couldn't figure out why. Thank you for commenting! – lobianco Jan 17 '18 at 18:06
  • tech4242 , can you please share GitHub sample project download link? – Dhananjay Patil Feb 01 '18 at 14:09
  • @DhananjayPatil incoming! I need to sit down this weekend – tech4242 Feb 01 '18 at 16:13
  • @Anthony I'll have a look again this weekend and hopefully I'll add it to the GH repo – tech4242 Feb 01 '18 at 16:13
  • 1
    @tech4242 Above example works well, but if I add javascript file same as styles.css it's not loading in html. – Dhananjay Patil Feb 02 '18 at 04:27
  • @DhananjayPatil Updated my answer with a sample project incl. local javascript files! feel free to star it ;) The magic is in adding a `WKPreferences()` with `preferences.javaScriptEnabled = true`. See the GH project for more info! – tech4242 Feb 08 '18 at 13:12
  • It's working for me, I have used local images from Document Directory using I have HTML from API and writing that HTML to Document directory with src="file:///var/mobile/Cont....../Documents/MyLocalAPI/1.png" [webView loadFileURL:[NSURL fileURLWithPath:htmlfilePath] allowingReadAccessToURL:[NSURL fileURLWithPath:documentsDirectoryPath isDirectory:YES]]; The issue was "isDirectory: YES", I haven't used and after using its work for me. – Darshit Mendapara Sep 29 '20 at 06:52
15

Swift 4 Method

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

Updated for Xcode 9.3

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. :)

JDev
  • 5,168
  • 6
  • 40
  • 61
3

This solution helped me:

[configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];
1

This works well (Swift 3, Xcode 8):

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!

    override func loadView() {
        webView = WKWebView()
        webView.navigationDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        if let url = Bundle.main.url(forResource: "file", withExtension: "txt")
        {
            do
            {
                let contents = try String(contentsOfFile: url.path)
                webView.loadHTMLString(contents, baseURL: url.deletingLastPathComponent())
            }
            catch
            {
                print("Could not load the HTML string.")
            }
        }
    }
}
hkdalex
  • 717
  • 7
  • 13
1

This works nicely with file URL or remote URL, and whether file is in the bundle or in documents:

if url.isFileURL {
    webView.loadFileURL(url, allowingReadAccessTo: url)
} else {
    let request = URLRequest(url: url)
    webView.load(request)
}
drewster
  • 5,460
  • 5
  • 40
  • 50
1

Constructing the URLs this way allowed me to load resources from the document directory with WKWebView:

guard let docDir = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
    return
}

let resourceURL = docDir.appendingPathComponent("/Path/To/Your/Resource")
self.wkWebView.loadFileURL(resourceURL, allowingReadAccessTo: docDir)
hgwhittle
  • 9,316
  • 6
  • 48
  • 60
  • with ur above solution I am perfectly able to load one single image into WKWebView from doc dir , but can you help me out I have an html which contains the. last path of the images and I have already downloaded the images in documents directory how can show that html images in WKWEBview – iMinion Sep 10 '20 at 17:53
0

The files must be in the document directory.

I implemented the following to retrieve a document:

let documentDirUrl = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileNameWithExtension = "IMG_0002.PNG"
let indexFileUrl = documentDirUrl.appendingPathComponent(fileNameWithExtension)
if FileManager.default.fileExists(atPath: indexFileUrl.path) {
    webView.loadFileURL(indexFileUrl, allowingReadAccessTo: documentDirUrl)
}
Prakash
  • 812
  • 6
  • 16
-1

Check that ticket: iOS: How to load local files (not in the bundle) in a WKWebView?

var nsurl = URL(fileURLWithPath: URL(fileURLWithPath: URL(fileURLWithPath: documentsDirectory()).appendingPathComponent(user_appli).absoluteString).appendingPathComponent("index.html").absoluteString) //locally
var readAccessToURL: URL? = nsurl.deletingLastPathComponent?.deletingLastPathComponent

if let anURL = readAccessToURL {
    webView?.loadFileURL(nsurl, allowingReadAccessTo: anURL)
}
ΩlostA
  • 2,501
  • 5
  • 27
  • 63