Loading local images into WKWebView - ios

I'm trying to get WKWebView to display locally downloaded images in a WKWebView. The webview normally displays HTML, which is retrieved remotely. The contents of the HTML can sometimes contain remote links to images. My app parses the HTML and looks for these HTML tags, downloads the file it is referencing and subsequently replaces the remote link with a local one.
Normally speaking, this wouldn't be very difficult but the images aren't being displayed, presumably due to the images and the local HTML files for the webview being in two separate directories (the documents directory and the app bundle directory respectively).
I've seen people suggest moving the download destination of the images to the same directory as where the HTML files are but this isn't an option for me as I don't want to start mixing up files downloaded by the user with local assets.
What would be my best course of action here?

Well, I've found a workaround. Instead of locally storing the images and referencing them in the HTML files, I'm now instead converting the images to Base64 and then adding them to the HTML. It's not ideal but it gets the job done. I'm going to leave this question open in case someone ever manages to find an actual solution.

To display cached HTML referencing cached resources in a WKWebView:
For each of the resources within your HTML content string, cache it into the directory as provided by NSTemporaryDirectory(). So an image tag like:
...<img src='https://www.myimage.com/example_image.png'/>...
should be cached and replaced into something like this:
...<img src='/private/var/mobile/Containers/Data/Application/527CF4FC-9319-4DFF-AB55-9E276890F5DC/tmp/example_image.png'/>...
Now cache the HTML content string with the replaced resource URLs. It must also be cached in the directory provided by NSTemporaryDirectory(). One difference here is that it must be cached (and later referenced) using the file:// protocol as a restriction of caching the string using NSData (see sample code).
For example file:///private/var/mobile/Containers/Data/Application/527CF4FC-9319-4DFF-AB55-9E276890F5DC/tmp/my_html_content_string.html
A few things to point out:
You cannot load the HTML as a raw string (loadHTMLString:baseURL:).
You cannot reference the cached resource within your HTML string using the file:// protocol. That may work in a UIWebView, but will not work in the WKWebView.
Objective-C
// To cache the HTML string:
NSString *HTML = <HTML CONTENT WITH CACHED RESOURCES>;
NSData *data = [HTML dataUsingEncoding: NSUTF8StringEncoding];
[data writeToURL: cachedHTMLURL atomically: YES];
// To load the store HTML file:
[myWKWebView loadRequest: [NSURLRequest requestWithURL: cachedHTMLURL]]; // (file://.../tmp/my_html_content_string.html)
Swift
// To cache the HTML string:
let HTML = <HTML CONTENT WITH CACHED RESOURCES>
let data = HTML.data(using: String.Encoding.utf8)
do {
try data.write(to: cachedHTMLURL, options: .atomic)
} catch {
print(error)
}
// To load the store HTML file:
myWKWebView.load(URLRequest(url: cachedHTMLURL)) // (file://.../tmp/my_html_content_string.html)

I had the same problem with WKWebView as it can not load both html strings and images at the same time for security purposes. I switched to UIWebView, which is deprecated, but I was able to load both html strings and referenced images at the same time.

I developed a definitive solution for the company I work for. But it relies on the html / javascript side. Anywhere inside your html code where you will reference to a local image <img src="..."/> you should set this "src" dynamically, and it will work seamlessly.
function getLocalURL(path) {
let origin = window.location.origin
if (origin == "file://") {
return origin + window.location.pathname.replace("/index.html","") + path
}
return path
}
You should, clearly, rename index.html to whatever is your main .htm(l) filename :)
Usage:
getLocalURL("/local_images/location_icon.png")
Will return a WKWebView working path for the referenced local image path:
"file:///Users/arthurdapaz/Library/Developer/CoreSimulator/Devices/5073AF19-26A0-460E-BC82-E89100B8E1AB/data/Containers/Data/Application/2B099343-0BF5-4849-B1C2-2512377A9772/Documents/distDriver/local_images/location_icon.png"

Related

Setting local image on WKWebview using evaluateJavascript method

I have a HTML file which contains local resource files such as css, js files inside its content. These local resource files are in zip format. My app use WKWebView to display this html file. I m trying to upload an image in one of the html image element on WKWebview which is saved locally and html is not updated with the image. I have a html image tag in local html like this.
<img id='image' style='display: block; height: 100%; width:100%; margin:0 auto;' />
And this image to load in this image tag is generated after the local html file is loaded in the WKWebview. Once the image is generated, I am trying to update the image tag with the evaluate javascript method like this.
NSString *js = #"document.getElementById('image').src='document directory/Library/images/generatedImage.png';"
[self.wkWebView evaluateJavaScript:js completionHandler:nil];
Am I doing something wrong here. I also read that we can't update the image locally which was 2 years ago, still the same? Is there any workaround solution for my scenario. Thanks in advance.
I can think of two ways to do what you want.
Custom URL Scheme
Convert the image to base64 then set it via evaluate Javascript
Custom URL Scheme
This is supported from iOS Version 11, if you are supporting version below 11 then you are better off with second approach. The basic idea is mention your URL with custom protocol like my-scheme://your-path then intercept that path and provide the response to WKWebView. In this you have to create a class that extends WKSchemeHandler and override it's required methods. e.g.
import WebKit
import Foundation
class CustomeSchemeHandler : NSObject,WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
}
}
In the start method you can load the content from your desired source and then return the response via
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
After implementing your logic, you need to register it with WKWebView via setURLSchemeHandler(_:forURLScheme:)
After seeing the above you can mention image URL with your custom scheme and leave the work to your CustomSchemeHandler.
This approach is very mature and can be really helpful in the long run. I found a very good and brief article that covers the implementation in details, do check it out: https://medium.com/#kumarreddy_b/custom-scheme-handling-in-uiwebview-wkwebview-bbeb2f3f6cc1
Convert the image to base64 then set it via evaluating Javascript
Create UIImage from your path e.g. https://stackoverflow.com/a/43006566/2299040
Convert the image to base64
Get the Id of your HTML image element and load the base64 String
e.g.
if let image = UIImage(contentsOfFile: ""){
//Now use image to create into NSData format
if let imageData = image.jpegData(compressionQuality: 0.8){
let strBase64 = imageData.base64EncodedString(options: .lineLength64Characters)
webView.evaluateJavaScript("document.getElementById('your-image-element-id').setAttribute('src', '\(strBase64)');")
}
}
For your example, I was able to load an image from the internet by setting that URL as the src.
I guess you might have not given read access to your local image file directory while loading the webview.
Utilize loadFileURL method of wkwebview to load an HTML file by giving access to a particular directory
For EG (in Swift),
let imgDir = "<your image directory>"
let imgDirUrl = URL(fileURLWithPath: String(format: "%#", imgDir), isDirectory: true)
webView.loadFileURL(<your HTML file URL path>, allowingReadAccessTo: imgDirUrl)
(In objc)
NSString *imgDir = "<your image directory>"
NSURL *imgDirUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#",imgDir] isDirectory:true];
[webView loadFileURL:<your HTML file URL path> allowingReadAccessToURL:imgDirUrl];

WKWebView fails to load images and CSS using loadHTMLString(_, baseURL:)

Apple's recommendation:
In apps that run in iOS 8 and later, use the WKWebView class instead of using UIWebView.
Thus, I have replaced my good old UIWebView with a shiny new WKWebView. But what I thought to be an easy exercise (simply swapping the classes and replacing the delegate methods) turned out to be a real mess.
The Problem
When I load an HTML string using
loadHTMLString(String, baseURL: URL?)
the web view loads and renders the pure HTML but it doesn't load any images or CSS files referenced inside the htmlString.
This happens only on a real device!
In Simultor all referenced resources are loaded correctly.
Example
I have defined a simple htmlString in my view controller class:
let imageName = "image.png"
let libraryURL: URL // The default Library URL
var htmlString: String {
return "<html> ... <img src=\"\(imageName)\" /> ... </html>"
// "..." represents more valid HTML code incl. header and body tags
}
The image is stored in the root Library folder so its URL is:
let imageURL = libraryURL.appendingPathComponent(imageName)
Now I load the htmlString into the web view:
webView.loadHTMLString(htmlString, baseURL: libraryURL)
and it doesn't load the image even though the baseURL is set correctly.
Ideas for a Solution
Maybe WKWebView has a problem with resolving relative paths so my first idea was to use absolute paths inside the HTML string instead.
→ ❌ Doesn't work.
Two answers to another SO post suggested that using
loadFileURL(URL, allowingReadAccessTo: URL)
instead of loadHTMLString(...) works in iOS 9+.
→ ✅ That works.
However, I cannot use solution 2 because my HTML files are encrypted and the decrypted files must not be stored on the disk.
Question
Is there any way to load local resources like images and styles using the WKWebView's
loadHTMLString(String, baseURL: URL?)
function? Or is still a bug in iOS 9+?
(I just cannot believe that Apple provides and recommends using a web view that cannot load any local web content from inside an HTML string?!)
Without taking a look at your actual project it's difficult to give some hundreed percent sure advices.
However:
class ViewController: UIViewController {
var webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
webView.translatesAutoresizingMaskIntoConstraints = false
let views = [
"webView" : webView
]
view.addSubview(webView)
var constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[webView]|", options: [.AlignAllLeading, .AlignAllTrailing], metrics: nil, views: views)
constraints.appendContentsOf(NSLayoutConstraint.constraintsWithVisualFormat("V:|[webView]|", options: [.AlignAllTop, .AlignAllBottom], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(constraints)
let path = NSBundle.mainBundle().pathForResource("ios - WKWebView fails to load images and CSS using loadHTMLString(_, baseURL_) - Stack Overflow", ofType: "htm")
let url = NSURL(fileURLWithPath: path!)
webView.loadHTMLString(try! String(contentsOfURL: url), baseURL: url.URLByDeletingLastPathComponent)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I think the key point here is baseUrl parameter, you should setup it correctly. In my case i've used html's url without last path component - e.g. containing folder. This works fine on both device & simulator - check device snapshot. I've uploaded sample project to https://github.com/soxjke/WKWebViewTest so you can take a look (i've removed codesigning info from git)
So, to recap - method is working, functionality is working, just you do something wrong with it. To help you get what's wrong with your solutions, i'll add some suggestions:
1. Remember, that simulator filesystem is case-insensitive, device filesystem is case-sensitive. So if you have your filenames in html in lowercase - this won't work on device. 8fFsD.png != 8ffsd.png
2. Remember, that when copying resources, XCode ignores your folder structure. So if your html has <img src="./img/1.png"> and your XCOde project has folder structure like
test.htm
img/
1.png
2.png
After build it will be flattened, so test.htm and 1.png and 2.png will reside on same level
test.htm
1.png
2.png
I'm almost sure, after you verify these two assumptions, you'll get this method working.
I had this problem today, I've found the solution and potentially the cause:
loadHTMLString(String, baseURL: URL?)
This function doesn't allow the rendered HTML to access local media, as far as I'm aware, this is because it would be an injection risk, this could allow rendered HTML to access and manipulate your local file system. With a html string, that could come from anywhere or anyone.
loadFileURL(URL, allowingReadAccessTo: URL)
With this function, you point the WKWebview to the html file in your FileManager, and to the containing folder with 'allowingReadAccessTo'. Because the html is stored within the FileManager, it will allow the rendered HTML to access locally stored media.
If you don't have the html file stored locally for some reason(I assume you do), You could write the html sting into a .html file, then point to the URL of that file. However, this is just subverting Apple's protection, so do it at your own peril (don't do it).
This is just the solution that worked for me and my understanding of why we're having the problem to begin with.
Edit #1: Typo.
Edit #2: I've since found another nuance, When stating the 'allowingReadAccessTo:' URL, if the HTML itself needs to access things in parent folders (ie: .css, .js files), you need to specify the parent folder, not necessarily the location of the HTML itself, this will then implicitly allow access to the child folders as required also. For me, this problem was only apparent on a physical device, this didn't seem to have an effect whilst running in simulator, likely another discrepancy between how permissions work on simulator and a physical device.
Personally, I had to switch to using XWebView as the out-of-the-box behavior of WKWebView does not allow loading of local files. XWebView tricks it by loading up a local web server in the background and directing local traffic thru it. (XWebView is based on top of WKWebView)
Seems a bit overkill, but that is what I ended up having to do.
I've been experimenting with this as well, with similar restrictions, and the problem appears to be that paths aren't resolved unless baseURL references the application bundle. It doesn't work if you, for example, have something in the application's documents.
Edit: I have filed a radar for this rdar://29130863
Well you should be able to use local images and CSS files (and JavaScript files for that matter) with WKWebViews with the function that you have already found. My guess is that the problem is with your baseURL variable.
Update 7.5.2017:
I have completely updated the code from another SO answer of mine that used to be linked to my answer here. I have a working project for loadHTMLString() and .loadFileURL()
Try to create baseURL using:
let baseURL = URL(fileURLWithPath: "#path#")
instead of:
let baseURL = URL(string: "#path#")
The main difference is that the first method adds file:// prefix before the path.
You can base64 encode the images... I know that works. Not sure if it will be appropriate for your use case though.
Kind of funny, I just ran into this problem while doing the opposite - moving from base64 encoded to image files.
When I used UIWebview, I used baseURL as,
let baseUrl = NSURL(string: Bundle.main.path(forResource: "cms", ofType: "html")!)! as URL
webView.loadHTMLString(bodyPage, baseURL: baseUrl)
But for the WKWebView, I used baseURL as
let baseUrl = Bundle.main.bundleURL
webView.loadHTMLString(bodyPage, baseURL: baseUrl)
This works for me.
I know this is quite old already, but I ran into the exact same problem and it took me hours of trials and even to find this thread with the same problem (Xamarin Forms App)
My issue was: parsing remote HTML content into a string and also adding locally saved images (also downloaded dynamically, no resource of the app). On the simulator all works well, but on acutal device the local images are not showing (also no ? or anything indicating an error, just a blank frame). The Xamarin webview also offers the "BaseURL" option which didn't help, also not to use the BaseURL on the custom iOS wkWebView.
The only working solution as pointed out by Scott above, is to write the HTML into a file and then use the "LoadFileUrl" function and allow read access to the base directory. This also works with absolute file paths for images in the HTML (not only relative to the basedir, but of course somewhere within the basedir).
My custom webview renderer to load web and local content looks like this now:
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
base.OnElementPropertyChanged(sender, e);
NSUrl baseURL = new NSUrl(App.dirNews, true);
string viewFile = Path.Combine(App.dirNews, "view.html");
NSUrl fileURL = new NSUrl(viewFile, false);
switch (e.PropertyName) {
case "Url":
System.Console.WriteLine("--- Loading Web page ---");
System.Console.WriteLine("--- " + Element.Url + " ---");
NSUrlRequest myRequest = new NSUrlRequest(new NSUrl(Element.Url), NSUrlRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData, 120);
Control.LoadRequest(myRequest);
break;
case "HTML":
System.Console.WriteLine("--- Showing HTTP content ---");
File.WriteAllText(viewFile, Element.HTML, System.Text.Encoding.UTF8);
Control.LoadFileUrl(fileURL, baseURL);
break;
}
}
I was able to reproduce a similar issue. WKWebView loads my images specially if they are located remotely, apart from my app server.
For servers that are not SSL-secured (http instead of https), you can set your info.plist as per below:
App Transport Security Settings
- Allow Arbitrary Loads in Web Content (Set to YES)
- Allow Arbitrary Loads (Set to YES)
The problem was actually in the server. The server application was either:
Changing the image src from "http://IP-or-domain/uploads/file.jpg" to "../../uploads/file.jpg"
- OR -
The image src was "http://localhost/uploads/file.jpg" or "http://127.0.0.1/uploads/file.jpg" instead of "http://YOUR-SERVER-IP-ADDRESS/uploads/file.jpg"
In these cases, the actual device wont be able to locate the image. This only works with iOS Simulator because the virtual device is the same as the server and development machine. It can read LOCALHOST and 127.0.0.1.
In my server, I was using a Rich Text Editor (TinyMCE) and it automatically removes the IP address after it detects that it's the same source.
WKWebView can load image or css file from NSTemporaryDirectory, so you can copy your files to NSTemporaryDirectory, and then load it. It works for me on iOS 14! see this issue. ios-wkwebview-loadhtmlstring-baseurl-fails-to-load-images-and-read-css
It took me a while to figure this out, but based on this answer I got it working:
https://stackoverflow.com/a/73519282/5868066
Try this:
let htmlPath = URL(fileURLWithPath: "")
let htmlDirectory = htmlPath.deletingLastPathComponent()
let htmlString = try! String(contentsOfFile: htmlPath.path, encoding: .utf8)
let baseURL = URL(fileURLWithPath: htmlDirectory)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[.zero]
webView.loadFileURL(htmlPath, allowingReadAccessTo: documentsDirectory)
webView.loadHTMLString(htmlString, baseURL: baseURL)

Parse - How to save a html file in iOS device

I'm using Parse for my iOS app and I'm trying to locally save my HTML file from Parse core data to my device's local storage. I'm doing this so the user can still access the data even when they're offline. I've read about local datastore and cache policy and have tried both. With local datastore, it doesn't let me load the HTML to my UIWebView but it loads everything else (probably due to the HTML needing internet to access it). Cache policy actually works but only accesses the more recent items (just as a cache should work).
I am asking on here to see what I can do to locally save these HTML files from Parse so a user can access them even when the internet is gone.
Thanks!
You can use PFFile's method getData to get NSData and save it to NSUserDefaults:
let data = file.getData()
NSUserDefaults.standardUserDefaults().setObject(data, forKey: file.name)
...and when you need to read it:
let data = NSUserDefaults.standardUserDefaults().dataForKey(file.name)
This can help you display the page in WebView: How do I convert HTML NSData to an NSString?
Update: UIWebView has method to show NSData: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIWebView_Class/#//apple_ref/occ/instm/UIWebView/loadData:MIMEType:textEncodingName:baseURL:

Convert Ios Photo Album path url same like as Document directory File-path

As per my requirement, want to access iOS device photos app file url (not file as data) for my GCDWebUploader. I want assets library url for my web server.
NSString* documentsPath =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
_webServer = [[GCDWebUploader alloc] initWithUploadDirectory: documentsPath];
// this is working and document directory files shown on browser.
_webServer = [[GCDWebUploader alloc] initWithUploadDirectory:assetsUrl]; // this is not working.Nothing shown on browser. //assetsUrl= assets library url for file from photos app
_webServer.delegate = self;
_webServer.allowHiddenItems = YES;
[_webServer start];
my web-server display all the photos app images and videos on pc browser if document directory.this functionality already done using GCDWebUploader. but I can't find asset url behave like file path.
I don't want to copy the photos app files into document-directory and use.but directly access from assets library.
I want assets url work same like document directory filepath. please help me for that.
An asset URL looks like this:
assets-library://asset/asset.JPG?id=CD12228F-0E99-4ABD-999D-6A76F54024E7&ext=JPG
This is an internal URL to ALAssetsLibrary which means nothing outside of this context. You can't expect to pass this URL to GCDWebServer and expect the server to magically do something with it.
Furthermore, GCDWebServer by definition can only serve URLs with the HTTP scheme, with the hostname matching your iPhone/iPad network name, and with paths for which you have implemented handlers.
For instance if you have implemented a GET handler for the path /photos/index.html, then connecting to your iPhone/iPad using your web browser at http://my-device.local/photos/index.html will call the corresponding handler on GCDWebServer, which then can return some content (like an HTML web page or an image file).
Connecting however to assets-library://asset/asset.JPG from your web browser doesn't mean anything and will fail.
Connecting to http://my-device.local/asset.JPG?id=CD12228F-0E99-4ABD-999D-6A76F54024E7&ext=JPG will also fail if you don't have a GET handler in GCDWebServer for that path.
So in a nutshell, to serve photos from ALAssetsLibrary using GCDWebServer, you can do it as such:
Implement a default handler to catch all GET requests
Implement a handler for GET requests to /index.html (you must add it to the GCDWebServer instance after the default handler)
In the implementation of the /index.html handler, you return an HTML web page that lists the URLs of the photo assets from ALAssetsLibrary, each of them having a relative URL link like My photo link (the path portion of the asset URL).
In the implementation of the default handler, you retrieve the path of the GCDWebServerRequest, prepend assets-library://asset, and that gives you back the original asset URL: assets-library://asset/asset.JPG?id=CD12228F-0E99-4ABD-999D-6A76F54024E7&ext=JPG. With this URL, you can finally retrieve the asset data, i.e. the JPEG image, and return it using a GCDWebServerDataResponse (don't forget to set the MIME type to image/jpeg).

Wrapping an NSString (file path) in NSURL breaks Unicode?

I've had an iPhone app in the store for 2 years that loaded local HTML files into a webview. While updating it I've run into an issue with the file paths. One of the elements in the path is in Russian, which still looks fine in _finalPath, but when I wrap it in an NSURL, the Cyrillic letters are converted to Unicode entities, breaking the path to the HTML file.
//finalPath is figured in and passed on from tenseListViewController
//ACK - this now seems to turn брать into %D0%B1%D1%80%D0%B0%D1%82%D1%8C in the middle of the path!
NSLog(#"-->starting showInfo: _finalPath is %#", _finalPath);
NSURL *url = [NSURL fileURLWithPath:_finalPath];
NSLog(#"*url is %#", url);
Here's the output in the log:
2013-06-05 13:51:39.409 NewTryout[29609:c07] -->starting showInfo: _finalPath is /Users/cford/Library/Application Support/iPhone Simulator/6.1/Applications/8C06E53D-BFA6-4EA5-823A-0EBDBB3B51B2/NewTryout.app/verbs/брать/imperfective/Present.html
2013-06-05 13:51:39.409 NewTryout[29609:c07] *url is file://localhost/Users/cford/Library/Application%20Support/iPhone%20Simulator/6.1/Applications/8C06E53D-BFA6-4EA5-823A-0EBDBB3B51B2/NewTryout.app/verbs/%D0%B1%D1%80%D0%B0%D1%82%D1%8C/imperfective/Present.html
Is this a change in iOS5 or 6, or should I be looking at something else in my code? Any suggestions?
Non-ASCII characters are simply not allowed in URLs to begin with (IRIs were invented to replace URLs, in particular to support Unicode). Your Russian characters are being encoded to UTF-8 octets and then those octets are being URL-encoded. That is correct behavior for URLs, and works just fine in HTML links. If the WebView is not opening the HTML file correctly when the link is clicked, then the WebView itself must be broken when processing URLs. It should be URL-decoding and UTF8-decoding the data before then using it. If it is not, then that is a bug in the WebView. Unless the WebView is just giving you the raw URL text when the link is clicked, and you are opening the file yourself, in which case it is your responsibility to decode the URL data before then using it in the rest of your code.

Resources