128

For previous iOS 8 betas, load a local web app (in Bundle) and it works fine for both UIWebView and WKWebView, and I even ported a web game using the new WKWebView API.

var url = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource("car", ofType:"html"))

webView = WKWebView(frame:view.frame)
webView!.loadRequest(NSURLRequest(URL:url))

view.addSubview(webView)

But in beta 4, I just got a blank white screen (UIWebView still work), looks like nothing is loaded or executed. I saw an error in the log:

Could not create a sandbox extension for /

Any help to guide me to the right direction? Thanks!

Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
Lim Thye Chean
  • 8,704
  • 9
  • 49
  • 88

14 Answers14

110

They finally solved the bug! Now we can use -[WKWebView loadFileURL:allowingReadAccessToURL:]. Apparently the fix was worth some seconds in WWDC 2015 video 504 Introducing Safari View Controller

https://developer.apple.com/videos/wwdc/2015/?id=504

For iOS8 ~ iOS10 (Swift 3)

As Dan Fabulish's answer states this is a bug of WKWebView which apparently is not being solved any time soon and as he said there is a work-around :)

I am answering just because I wanted to show the work-around here. IMO code shown in https://github.com/shazron/WKWebViewFIleUrlTest is full of unrelated details most people are probably not interested in.

The work-around is 20 lines of code, error handling and comments included, no need of a server :)

func fileURLForBuggyWKWebView8(fileURL: URL) throws -> URL {
    // Some safety checks
    if !fileURL.isFileURL {
        throw NSError(
            domain: "BuggyWKWebViewDomain",
            code: 1001,
            userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("URL must be a file URL.", comment:"")])
    }
    try! fileURL.checkResourceIsReachable()

    // Create "/temp/www" directory
    let fm = FileManager.default
    let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("www")
    try! fm.createDirectory(at: tmpDirURL, withIntermediateDirectories: true, attributes: nil)

    // Now copy given file to the temp directory
    let dstURL = tmpDirURL.appendingPathComponent(fileURL.lastPathComponent)
    let _ = try? fm.removeItem(at: dstURL)
    try! fm.copyItem(at: fileURL, to: dstURL)

    // Files in "/temp/www" load flawlesly :)
    return dstURL
}

And can be used as:

override func viewDidLoad() {
    super.viewDidLoad()
    var fileURL = URL(fileURLWithPath: Bundle.main.path(forResource:"file", ofType: "pdf")!)

    if #available(iOS 9.0, *) {
        // iOS9 and above. One year later things are OK.
        webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
    } else {
        // iOS8. Things can (sometimes) be workaround-ed
        //   Brave people can do just this
        //   fileURL = try! pathForBuggyWKWebView8(fileURL: fileURL)
        //   webView.load(URLRequest(url: fileURL))
        do {
            fileURL = try fileURLForBuggyWKWebView8(fileURL: fileURL)
            webView.load(URLRequest(url: fileURL))
        } catch let error as NSError {
            print("Error: " + error.debugDescription)
        }
    }
}
Community
  • 1
  • 1
nacho4d
  • 43,720
  • 45
  • 157
  • 240
  • I'm guessing this works because maybe with normal remote websites its downloading the files into the TMP directory anyways, right? So dumping your files there makes it treat it as if it did download that. It sucks to have to copy all those files, but I guess it only has to happen once during start up. If you did it this way too, it wouldn't be too hard to switch things out when this bug is fixed, as the only thing that would change is not copying the files, and perhaps a different api call based on the description of the wkwebkit fix. – AwDogsGo2Heaven Feb 24 '15 at 06:43
  • Is it safe to store data in the `NSTemporaryDirectory()` directory? What happens if the device runs low on space? Will iOS remove files from the temporary directories? – Stefan Arentz Feb 25 '15 at 02:59
  • As its name suggests it is temporary. So before showing something you should check it exists. The OS wipes that directory from time to time. – nacho4d Feb 25 '15 at 03:22
  • 2
    Some modifications for it to copy the entire folder, images and all. `let orgFolder = NSBundle.mainBundle().resourcePath! + "/www"; var newFilePath = pathForBuggyWKWebView(orgFolder) self.loadingWebView!.loadRequest(NSURLRequest(URL: NSURL.fileURLWithPath(newFilePath!+"/Loading.html")!))` – Piwaf Jul 09 '15 at 22:18
  • 1
    Piwaf's solution worked slightly better for me, note that it should be "self.webView" and not "self.loadingWebView" given the example above though – Patrick Fabrizius Aug 05 '15 at 20:37
  • Does this workaround work on the simulator? It seems to be failing on the copyItemAtPath. Perhaps I can't write to that directory on the simulator? – jvoll Aug 07 '15 at 00:00
  • In the simulator you don't this workaround. You can place an #ifdef inside pathForBuggyWKWebView and return `filePath` without doing anything :) – nacho4d Aug 07 '15 at 00:38
  • 2
    Thanks @nacho4d. tldr; the /tmp folder solution will not work on 8.0 but will on 8.0.2. I was having trouble getting this solution to work on my test device and eventually cloned shazron's repo to give it a try. That didn't work either. Turns out that shazron's solution doesn't work on the version of iOS my device was running 8.0 (12A366) on iPhone 6 Plus. I tried it on a device (iPad Mini) running iOS 8.0.2 and it works fine. – jvoll Aug 07 '15 at 21:31
  • 1
    it should be let _ = try? fm.removeItemAtURL(dstURL) instead of let _ = try? fileMgr.removeItemAtURL(dstURL) – DàChún Feb 19 '16 at 19:24
  • Thanks, I just fixed it! – nacho4d Feb 21 '16 at 12:54
  • 3
    Only for simple websites. If you are using ajax or loading local views via angular, expect "Cross origin requests are only supported for HTTP". Your only fallback is the local webserver approach which i do not like since it is visible on the local network. This needs noting the in the post, save folk some hours. – jenson-button-event Mar 23 '16 at 08:12
  • 1
    What if the `index.html` needs to be dynamically generated, such as being a template with placeholders then reused for different content records? I guess I'm forced to use `loadHTMLString`, but then local `css` and `js` files stop working? – TruMan1 May 03 '16 at 18:52
  • @jenson-button-event have you found any other solutions to deal with CORS issues with local files on iOS 9+? As you mentioned, loadFileURL:allowReadAccess does not seem to help in those cases. – Rodrigo Lima Jun 25 '16 at 22:59
  • 1
    @RodrigoLima - no - went with the local web server approach. had no issues with it. I just don't like the fact the server is publicly visible. – jenson-button-event Jun 26 '16 at 06:01
  • For those wondering why their `-[WKWebView loadFileURL:allowingReadAccessToURL:]` doesn't work: make sure to check you give it "standard" file URL. I could not get it working with URL built with `URL(string: path, relativeTo: fileURL)`. (And yes, isFileURL returned true for it/it worked in Simulator but *not on the device*). Applying `.standardizedFileURL` to the URL solved the issue for me. – Grigory Entin Jan 31 '19 at 21:45
  • @GrigoryEntin I have a very similar issue but standardizedURL doesn't help :(. more details there: https://stackoverflow.com/questions/58437328/cannot-load-a-local-html-file-into-a-wkwebview-on-device-only-works-on-simulato – Żabojad Oct 17 '19 at 21:19
  • @nacho4d the above solution does not work for ios13 if u could help me out https://stackoverflow.com/questions/63800637/unable-to-load-html-with-only-images-in-wkwebview-ios-13-swift-5 – iMinion Sep 08 '20 at 20:44
85

WKWebView can't load content from file: URLs via its loadRequest: method. http://www.openradar.me/18039024

You can load content via loadHTMLString:, but if your baseURL is a file: URL, then it still won't work.

iOS 9 has a new API that will do what you want, [WKWebView loadFileURL:allowingReadAccessToURL:].

There is a workaround for iOS 8, demonstrated by shazron in Objective-C here https://github.com/shazron/WKWebViewFIleUrlTest to copy files into /tmp/www and load them from there.

If you're working in Swift, you could try nachos4d's sample instead. (It's also much shorter than shazron's sample, so if you're having trouble with shazron's code, give that a try instead.)

Community
  • 1
  • 1
Dan Fabulich
  • 37,506
  • 41
  • 139
  • 175
  • 1
    One workaround (mentioned here: https://devforums.apple.com/message/1051027) is to move content into tmp and access it from there. My quick test seems to indicate that that it does work... – Mike M Oct 02 '14 at 18:38
  • Hi @Dan. I downloaded your example from git. Thank you for that. I seem to get a valid URL, with all needed files copied to a new folder in /Documents. But the webview now tries to load the page forever. Any clue? I had already posted a question here: http://stackoverflow.com/q/26455432/873436, if you prefer to answer there... – invalidArgument Oct 20 '14 at 03:40
  • I tried moving to /tmp, but seems still blank, anybody tried and succeed? – xhg Dec 12 '14 at 21:21
  • Does the /tmp/ workaround work on 8.0? It's not working for me. – geppy Dec 13 '14 at 23:33
  • 1
    That *sample* is WAY TOO MUCH code just for a demo. The file to be read must be inside `/tmp/www/` . Use `NSTemporaryDirectory()` and `NSFileManager` to create `www` directory (because there is no such directory by default). Then copy your file in there and now read this file :) – nacho4d Feb 23 '15 at 08:57
  • This sounds more like a choice than a bug to me. – Steven Fisher Apr 23 '15 at 16:31
  • 1
    Seems to be fixed in 9.0 – Karol Klepacki Jun 09 '15 at 09:50
  • @KarolKlepacki are you sure? From the bug comments it seems to suggest it is only partially fixed? [http://www.openradar.me/18039024#ag9zfm9wZW5yYWRhci1ocmRyFAsSB0NvbW1lbnQYgICA4LO39AoM](http://www.openradar.me/18039024#ag9zfm9wZW5yYWRhci1ocmRyFAsSB0NvbW1lbnQYgICA4LO39AoM) – JKennedy Mar 17 '16 at 12:30
8

An example of how to use [WKWebView loadFileURL:allowingReadAccessToURL:] on iOS 9.

When you are moving the web folder to a project, select "Create folder references"

enter image description here

Then use code that is something like this(Swift 2):

if let filePath = NSBundle.mainBundle().resourcePath?.stringByAppendingString("/WebApp/index.html"){
  let url = NSURL(fileURLWithPath: filePath)
  if let webAppPath = NSBundle.mainBundle().resourcePath?.stringByAppendingString("/WebApp") {
    let webAppUrl = NSURL(fileURLWithPath: webAppPath, isDirectory: true)
    webView.loadFileURL(url, allowingReadAccessToURL: webAppUrl)
  }
}

In the html file use filepaths like this

<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">

not like this

<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">

An example of directory that is moved to a xcode project.

enter image description here

Koodimetsa
  • 760
  • 9
  • 11
6

Temporary workaround: I'm using GCDWebServer, as suggested by GuidoMB.

I first find the path of my bundled "www/" folder (which contains an "index.html"):

NSString *docRoot = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"www"].stringByDeletingLastPathComponent;

... then start it up like so:

_webServer = [[GCDWebServer alloc] init];
[_webServer addGETHandlerForBasePath:@"/" directoryPath:docRoot indexFilename:@"index.html" cacheAge:3600 allowRangeRequests:YES];
[_webServer startWithPort:port bonjourName:nil];

To stop it:

[_webServer stop];
_webServer = nil;

Performance appears fine, even on an iPad 2.


I did notice a crash after the app goes into the background, so I stop it on applicationDidEnterBackground: and applicationWillTerminate:; I start/restart it on application:didFinishLaunching... and applicationWillEnterForeground:.

EthanB
  • 4,239
  • 1
  • 28
  • 46
  • 1
    Using a "server" probably eats up any performance benefits that `WKWebView` provides over `UIWebView`. Might as well stick with the old API until this is fixed. – ray Nov 12 '14 at 04:07
  • @ray Not with a Single-Page App. – EthanB Nov 12 '14 at 13:28
  • I've gotten GCDWebServer to work too, in internal builds of my app. And if there's a lot of javascript in your app, the server is totally worth it. But there are [other](http://stackoverflow.com/questions/25938393/text-selection-in-wkwebview-wkselectiongranularitycharacter) [issues](http://stackoverflow.com/questions/26046098/wkwebview-and-uimenucontroller) that prevent me from using WKWebView right now, so I'm hoping for improvements in iOS 9. – Tom Hamming Jan 21 '15 at 17:22
  • And how to show it on a WebView? I really don't understand what GCDWebServer is for? – chipbk10 Mar 30 '15 at 11:53
  • 1
    Instead of pointing the webview to "file://.....", you point it to "http ://localhost:/...". – EthanB Mar 30 '15 at 17:22
6
[configuration.preferences setValue:@"TRUE" forKey:@"allowFileAccessFromFileURLs"];

This solved the problem for me iOS 8.0+ dev.apple.com

also this seems to worked just fine too...

NSString* FILE_PATH = [[[NSBundle mainBundle] resourcePath]
                       stringByAppendingPathComponent:@"htmlapp/FILE"];
[self.webView
    loadFileURL: [NSURL fileURLWithPath:FILE_PATH]
    allowingReadAccessToURL: [NSURL fileURLWithPath:FILE_PATH]
];
nullqube
  • 2,959
  • 19
  • 18
  • instead of FILE you can put DIR too . – nullqube Aug 06 '17 at 21:40
  • This post has more info (including links to webkit source): https://stackoverflow.com/questions/36013645/setting-disable-web-security-and-allow-file-access-from-files-in-ios-wkwebvi/41266699#41266699 – plivesey Oct 24 '17 at 00:01
  • configuration.preferences setValue will give crash on iOS 9.3 – Vignesh Kumar Nov 19 '19 at 04:18
  • I'm using the iOS 13 SDK but trying to keep compatibility with iOS 8, and the `allowFileAccessFromFileURLs` approach crashes with `NSUnknownKeyException`. – arlomedia Apr 28 '20 at 01:06
  • this worked for me only if I replaced "productURL" with "FILE_PATH" – Curtis May 18 '20 at 00:57
4

Besides solutions mentioned by Dan Fabulich, XWebView is another workaround. [WKWebView loadFileURL:allowingReadAccessToURL:] is implemented through extension.

soflare
  • 751
  • 5
  • 16
  • 1
    I decided to use XWebView after looking at other work-arounds for this problem. XWebView is a framework implemented in Swift but I had no problem using it in my iOS 8 Objective-C app. – chmaynard May 30 '15 at 10:16
4

I cannot comment yet, so I am posting this as a separate answer.

This is an objective-c version of nacho4d's solution. The best workaround I've seen so far.

- (NSString *)pathForWKWebViewSandboxBugWithOriginalPath:(NSString *)filePath
{
    NSFileManager *manager = [NSFileManager defaultManager];
    NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"www"];
    NSError *error = nil;

    if (![manager createDirectoryAtPath:tempPath withIntermediateDirectories:YES attributes:nil error:&error]) {
        NSLog(@"Could not create www directory. Error: %@", error);

        return nil;
    }

    NSString *destPath = [tempPath stringByAppendingPathComponent:filePath.lastPathComponent];

    if (![manager fileExistsAtPath:destPath]) {
        if (![manager copyItemAtPath:filePath toPath:destPath error:&error]) {
            NSLog(@"Couldn't copy file to /tmp/www. Error: %@", error);

            return nil;
        }
    }

    return destPath;
}
Community
  • 1
  • 1
Faryar
  • 61
  • 4
  • 1
    I can't get this to work on iOS 8 in simulator. I want to put an .png in /tmp/www and then use img tag in my html. What should I use for the img tag src? – user3246173 Aug 04 '15 at 21:18
4

In the case that you are trying to display a local image in the middle of a larger HTML string like: <img src="file://...">, it still does not appear on device so I loaded the image file into NSData and was able to display it by replacing the src string with the data itself. Sample code to help build the HTML string to load into WKWebView, where result is what will replace what's inside the quotes of src="":

Swift:

let pathURL = NSURL.fileURLWithPath(attachmentFilePath)
guard let path = pathURL.path else {
    return // throw error
}
guard let data = NSFileManager.defaultManager().contentsAtPath(path) else {
    return // throw error
}

let image = UIImage.init(data: data)
let base64String = data.base64EncodedStringWithOptions(.Encoding64CharacterLineLength)
result += "data:image/" + attachmentType + "base64," + base64String

var widthHeightString = "\""
if let image = image {
    widthHeightString += " width=\"\(image.size.width)\" height=\"\(image.size.height)\""
}

result += widthHeightString

Objective-C:

NSURL *pathURL = [NSURL fileURLWithPath:attachmentFilePath];
NSString *path = [pathURL path];
NSData *data = [[NSFileManager defaultManager] contentsAtPath:path];

UIImage *image = [UIImage imageWithData:data];
NSString *base64String = [data base64EncodedStringWithOptions:0];
[result appendString:@"data:image/"];
[result appendString:attachmentType]; // jpg, gif etc.
[result appendString:@";base64,"];
[result appendString:base64String];

NSString *widthHeightString = @"\"";
if (image) {
    widthHeightString = [NSString stringWithFormat:@"\" width=\"%f\" height=\"%f\"", image.size.width, image.size.height];
}
[result appendString:widthHeightString];
  • in the swift version, please add a semicolon before base64. `result += "data:image/" + attachmentType + ";base64," + base64String` – shahil Feb 28 '19 at 00:46
  • Thanks, this is the only thing that worked for me, because I download a .gif file to a temp folder on the device, and then load that file into the `WKWebView` as an `` within a HTMLstring. – hotdogsoup.nl Mar 05 '20 at 18:59
1

I'm using the below. Has some extra stuff I'm working on but you can see where I've commented out the loadRequest and am substituting loadHTMLString call. Hope this helps until they fix the bug.

import UIKit
import WebKit

class ViewController: UIViewController, WKScriptMessageHandler {

    var theWebView: WKWebView?

    override func viewDidLoad() {
        super.viewDidLoad()

        var path = NSBundle.mainBundle().pathForResource("index", ofType: "html", inDirectory:"www" )
        var url = NSURL(fileURLWithPath:path)
        var request = NSURLRequest(URL:url)
        var theConfiguration = WKWebViewConfiguration()

        theConfiguration.userContentController.addScriptMessageHandler(self, name: "interOp")

        theWebView = WKWebView(frame:self.view.frame, configuration: theConfiguration)

        let text2 = String.stringWithContentsOfFile(path, encoding: NSUTF8StringEncoding, error: nil)

        theWebView!.loadHTMLString(text2, baseURL: nil)

        //theWebView!.loadRequest(request)

        self.view.addSubview(theWebView)


    }

    func appWillEnterForeground() {

    }

    func appDidEnterBackground() {

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func userContentController(userContentController: WKUserContentController!, didReceiveScriptMessage message: WKScriptMessage!){
        println("got message: \(message.body)")

    }

}
Dustin Nielson
  • 1,264
  • 1
  • 8
  • 8
1

For who must workaround this issue under iOS8:

If your page is not complicated, you might choose to make the page as a Single Page Application.

In other words, to embed all the resources into the html file.

To do: 1. copy your js/css file's content into / tags in the html file respectively; 2. convert your image files into svg to replace the accordingly. 3. load the page as before, using [webView loadHTMLString: baseURL:], for example

It was a bit different to styling a svg image, but it should not block you so much.

It seemed that the page render performance decreased a bit, but it was worthy to have such a simple workaround worked under iOS8/9/10.

firebear
  • 774
  • 1
  • 8
  • 19
0

In the same line of GCDWebServer, I am using SImpleHttpServer (http://www.andyjamesdavies.com/blog/javascript/simple-http-server-on-mac-os-x-in-seconds) and then loadRequest with the localhost url. With this approach you do not have to add any library, but the website files won't be in the bundle so It will not be deliverable. Because of that, this would be more appropriate for Debug cases.

toupper
  • 626
  • 7
  • 18
0

I’ve managed to use PHP’s web server on OS X. Copying to the temporary/www directory did not work for me. The Python SimpleHTTPServer complained about wanting to read MIME types, probably a sandboxing issue.

Here’s a server using php -S:

let portNumber = 8080

let task = NSTask()
task.launchPath = "/usr/bin/php"
task.arguments = ["-S", "localhost:\(portNumber)", "-t", directoryURL.path!]
// Hide the output from the PHP server
task.standardOutput = NSPipe()
task.standardError = NSPipe()

task.launch()
Patrick Smith
  • 518
  • 6
  • 9
0

@nacho4d solution is good. I want to change it a little but I don't know how to change it in your post. So I put it here I hope you don't mind. thanks.

In case you have a www folder there are many other files such as png, css, js etc. Then you have to copy all files to tmp/www folder. for example, you have a www folder like this: enter image description here

then in Swift 2.0:

override func viewDidLoad() {
    super.viewDidLoad()

    let path = NSBundle.mainBundle().resourcePath! + "/www";
    var fileURL = NSURL(fileURLWithPath: path)
    if #available(iOS 9.0, *) {
        let path = NSBundle.mainBundle().pathForResource("index", ofType: "html", inDirectory: "www")
        let url = NSURL(fileURLWithPath: path!)
        self.webView!.loadRequest(NSURLRequest(URL: url))
    } else {
        do {
            fileURL = try fileURLForBuggyWKWebView8(fileURL)
            let url = NSURL(fileURLWithPath: fileURL.path! + "/index.html")
            self.webView!.loadRequest( NSURLRequest(URL: url))
        } catch let error as NSError {
            print("Error: \(error.debugDescription)")
        }
    }
}

the function fileURLForBuggyWKWebView8 is copied from @nacho4d:

func fileURLForBuggyWKWebView8(fileURL: NSURL) throws -> NSURL {
    // Some safety checks
    var error:NSError? = nil;
    if (!fileURL.fileURL || !fileURL.checkResourceIsReachableAndReturnError(&error)) {
        throw error ?? NSError(
            domain: "BuggyWKWebViewDomain",
            code: 1001,
            userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("URL must be a file URL.", comment:"")])
    }

    // Create "/temp/www" directory
    let fm = NSFileManager.defaultManager()
    let tmpDirURL = NSURL.fileURLWithPath(NSTemporaryDirectory())
    try! fm.createDirectoryAtURL(tmpDirURL, withIntermediateDirectories: true, attributes: nil)

    // Now copy given file to the temp directory
    let dstURL = tmpDirURL.URLByAppendingPathComponent(fileURL.lastPathComponent!)
    let _ = try? fm.removeItemAtURL(dstURL)
    try! fm.copyItemAtURL(fileURL, toURL: dstURL)

    // Files in "/temp/www" load flawlesly :)
    return dstURL
}
DàChún
  • 4,751
  • 1
  • 36
  • 39
-1

Try using

[webView loadHTMLString:htmlFileContent baseURL:baseURL];

Seems it's still working. Yet.

Kampai
  • 22,848
  • 21
  • 95
  • 95
Oleksii
  • 66
  • 2
  • 3
    This only works for HTML file itself not the resources, right? – Lim Thye Chean Jul 25 '14 at 05:19
  • 1
    Unfortunately yes, only the HTML file seems to be loading. Let's hope that it's just a bug, and not new restriction to load local files. I'm trying to find this bug in the WebKit sources. – Oleksii Jul 25 '14 at 19:12
  • 1
    I've run into the same issue - HTML files are loaded but images and other resources that are on the local filesystem are not loaded. In the console, WebKit is throwing an error "not allowed to load local resource". I filed a bug for this, radar# 17835098 – mszaro Aug 04 '14 at 21:46