WKWebView not loading local files under iOS 8
Asked Answered
S

14

129

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!

Seducer answered 22/7, 2014 at 8:50 Comment(16)
Also try adding the webView to the view hierarchy in viewDidLoad, and load the request in viewWillAppear. My WKWebView is still working, but that is how I have it. Perhaps the WebView has an optimization to not load requests if they aren't in a view hierarchy?Meryl
Done (the view.addView in viewDidLoad and loadRequest in viewWillAppear), and I got the same white screen and same error message.Seducer
Hmm, I wonder if it is a swift bug. Mine is in objective c and working. Are you using the delegate methods to see if it is failing? Maybe it can't find the file?Meryl
Still does not seem to work in beta 5.Seducer
This only seems to happen on the device because on the simulator works fine. I am using Objc by the way.Pineal
This has to be a bug. Let's all file radars.Apriorism
This remains to be an issue in XCode 6 - beta 7. My temporary solution was to use github.com/swisspol/GCDWebServer to serve local files.Pineal
Think this is an iOS bug, not Xcode bug. Since iOS 8 beta 5 still have this issue, this is not going to get fixed by Xcode update.Seducer
... Still happening in the iOS 8 Gold Master. :(Tirewoman
So we can't use web technologies to write app. :(Seducer
Please, let's all file radars, the more the better. You can dupe mine when filing at Apple: openradar.me/radar?id=5839348817723392 Related to this file url bug in WKWebView: openradar.me/radar?id=5834555097350144Treadway
Remains to be an issue in iOS 8 finalPineal
Looks like a fix is on its way. trac.webkit.org/changeset/174029Apriorism
Anybody tested it under iOS 8.0.1?Seducer
Just tested on 8.2 beta and still fails. Obviously this is not a priority for Apple.Pineal
In iOS9 they said it is solved. Please see my answer belowTarryn
T
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)
        }
    }
}
Tarryn answered 23/2, 2015 at 14:51 Comment(17)
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.Emrich
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?Fraise
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.Tarryn
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")!))Exorbitant
Piwaf's solution worked slightly better for me, note that it should be "self.webView" and not "self.loadingWebView" given the example above thoughPuisne
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?Crescantia
In the simulator you don't this workaround. You can place an #ifdef inside pathForBuggyWKWebView and return filePath without doing anything :)Tarryn
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.Crescantia
it should be let _ = try? fm.removeItemAtURL(dstURL) instead of let _ = try? fileMgr.removeItemAtURL(dstURL)Bacchant
Thanks, I just fixed it!Tarryn
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.Bomke
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?Sabir
@Bomke 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.Sweettempered
@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.Bomke
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.Idalia
@GrigoryEntin I have a very similar issue but standardizedURL doesn't help :(. more details there: #58437828Hoax
@Tarryn the above solution does not work for ios13 if u could help me out #63801137Thaine
P
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.)

Panfish answered 26/9, 2014 at 7:22 Comment(8)
One workaround (mentioned here: 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...Alysaalyse
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: https://mcmap.net/q/175543/-wkwebview-showing-blank-on-device-working-on-simulator/873436, if you prefer to answer there...Epilepsy
I tried moving to /tmp, but seems still blank, anybody tried and succeed?Hetaera
Does the /tmp/ workaround work on 8.0? It's not working for me.Rosie
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 :)Tarryn
This sounds more like a choice than a bug to me.Fuliginous
Seems to be fixed in 9.0Quiz
@KarolKlepacki are you sure? From the bug comments it seems to suggest it is only partially fixed? http://www.openradar.me/18039024#ag9zfm9wZW5yYWRhci1ocmRyFAsSB0NvbW1lbnQYgICA4LO39AoMYonkers
S
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

Sheryllshetland answered 7/3, 2016 at 19:7 Comment(0)
T
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:.

Tirewoman answered 20/10, 2014 at 22:13 Comment(5)
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.Asbestosis
@Asbestosis Not with a Single-Page App.Tirewoman
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 issues that prevent me from using WKWebView right now, so I'm hoping for improvements in iOS 9.Apriorism
And how to show it on a WebView? I really don't understand what GCDWebServer is for?Rout
Instead of pointing the webview to "file://.....", you point it to "http ://localhost:<port>/...".Tirewoman
D
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]
];
Dysphoria answered 6/8, 2017 at 21:38 Comment(5)
instead of FILE you can put DIR too .Dysphoria
This post has more info (including links to webkit source): #36014145Leibniz
configuration.preferences setValue will give crash on iOS 9.3Sartin
I'm using the iOS 13 SDK but trying to keep compatibility with iOS 8, and the allowFileAccessFromFileURLs approach crashes with NSUnknownKeyException.Welter
this worked for me only if I replaced "productURL" with "FILE_PATH"Underbred
R
4

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

Rightwards answered 28/4, 2015 at 17:10 Comment(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.Sangfroid
B
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;
}
Bowlin answered 20/6, 2015 at 4:32 Comment(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?Aldis
C
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];
Crake answered 25/5, 2016 at 15:10 Comment(2)
in the swift version, please add a semicolon before base64. result += "data:image/" + attachmentType + ";base64," + base64StringLoveliesbleeding
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 <img> within a HTMLstring.Katharinakatharine
M
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)")

    }

}
Marylinmarylinda answered 23/7, 2014 at 23:17 Comment(1)
This seems to work only for the HTML files but not other resources.Seducer
M
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.

Musgrove answered 19/12, 2016 at 15:32 Comment(0)
D
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.

Diaspore answered 12/12, 2014 at 14:23 Comment(0)
H
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()
Hurtle answered 17/3, 2015 at 10:13 Comment(0)
I
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
}
Insertion answered 20/2, 2016 at 20:14 Comment(0)
C
-1

Try using

[webView loadHTMLString:htmlFileContent baseURL:baseURL];

Seems it's still working. Yet.

Creosol answered 22/7, 2014 at 17:47 Comment(3)
This only works for HTML file itself not the resources, right?Seducer
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.Creosol
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# 17835098Guardhouse

© 2022 - 2024 — McMap. All rights reserved.