I am trying to open an Excel document that is located on a server. I wrote the following code but it always returns false for UIApplication.shared.canOpenURL(url as URL)
I think I am missing some requirement for deep linking to Excel. Why is iOS not able to understand ms-excel:ofe|u| format?
#objc static func openExcel() {
let originalString = "http://s000.tinyupload.com/download.php?file_id=23290165129849240725&t=2329016512984924072514118"
let encodedString = originalString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
let encodedURLString = "ms-excel:ofe|u|" + encodedString! + "|n|TestDoc.xlsx|a|App"
if let url = NSURL(string: encodedURLString),
UIApplication.shared.canOpenURL(url as URL) {
UIApplication.shared.openURL(url as URL)
} else if let itunesUrl = NSURL(string: "https://itunes.apple.com/us/app/microsoft-excel/id586683407?mt=8&uo=4"), UIApplication.shared.canOpenURL(itunesUrl as URL) {
UIApplication.shared.openURL(itunesUrl as URL)
}
}
I have analyzed your code and found some mistakes. First, your URL was redirecting to somewhere, as per Microsoft documentation it can't handle redirecting URL's
The URL has to be encoded and must be a direct link to the file (not a
redirect). If the URL is in a format that Office cannot handle, or the
download simply fails, Office will not return the user to the invoking
application.
Here is Microsoft Documentation Link
The second mistake was you are only encoding the URL string containing site URL, you should consider the part after the scheme ms-excel: as a URL and should be encoded.
Because of improper encoding the let url = URL(string: encodedURLString) results nil that's why it is not working as expected.
Here is an example working code:
#objc static func openExcel() {
//replace the below url with yours. may be this one dosen't work
let originalString = "ofe|u|https://pgcconline.blackboard.com/webapps/dur-browserCheck-bb_bb60/samples/sample.xlsx"
let encodedString = originalString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let encodedURLString = "ms-excel:" + encodedString!
if let url = URL(string: encodedURLString),
UIApplication.shared.canOpenURL(url) {
UIApplication.shared.openURL(url)
} else if let itunesUrl = NSURL(string: "https://itunes.apple.com/us/app/microsoft-excel/id586683407?mt=8&uo=4"), UIApplication.shared.canOpenURL(itunesUrl as URL) {
UIApplication.shared.openURL(itunesUrl as URL)
}
}
Note: From iOS 9 you must whitelist any URL schemes your App wants to query in Info.plist under the LSApplicationQueriesSchemes key (an array of strings):
For example in our case:
When i try to open the URL in the question above I get redirected to this URL, so my guess would be that your code is fine, it just might be that your excel file you're trying to open is really an HTML page since tinyupload apparently blocks direct links to the files.
Maybe try opening a direct excel file download link, https://pgcconline.blackboard.com/webapps/dur-browserCheck-bb_bb60/samples/sample.xlsx (it was the first google result for 'xlsx file sample download')
Issue
I am receiving the following error:
[Error] ERROR – Error: Uncaught (in promise): SecurityError (DOM Exception 18): Blocked attempt to use history.replaceState() to change session history URL from file:///Users//Library/Developer/CoreSimulator/Devices//data/Containers/Bundle/Application//ios-shell.app/public/index.html to file:///Users//Library/Developer/CoreSimulator/Devices//data/Containers/Bundle/Application//ios-shell.app/public/#/. Paths and fragments must match for a sandboxed document.
The difference between the two urls is just the end public/index.html -> public/#/
There was a similar question asked here on StackOverflow however this was an issue between file:/// and file://
Implementation
This is a custom iOS application created to run HTML SPA applications without cordova. Currently running in the Simulator on an iPhone 8 device on iOS 11.2 using the WKWebView
The web application is an Angular 5 application using {hash: true} as the routing strategy.
ViewController
class ViewController: UIViewController {
var serenity: Serenity?
override func viewDidLoad() {
super.viewDidLoad()
serenity = Serenity(self)
serenity?.load("index")
}
...
}
Serenity.swift
class Serenity: WKWebView, WKScriptMessageHandler{
var controller: ViewController?
let config = WKWebViewConfiguration()
var commands = Dictionary<String, (_ args: Any?) -> Any?>()
init(_ controller: ViewController){
self.controller = controller
config.userContentController.addUserScript(WKUserScript(source: "window.NATIVE_DEVICE=true;", injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false))
super.init(frame: controller.view.frame, configuration: config)
}
func load(_ file: String) {
if let html = Bundle.main.path(forResource: file, ofType: "html", inDirectory: "public"){
let url = URL(fileURLWithPath: html)
let request = URLRequest(url: url)
super.load(request)
controller?.view.addSubview(self)
}
}
...
}
Question
Is there a way to load the index.html file as public/ so that when angular takes over and starts routing it doesn't cause this error?
Any other suggestions would be helpful as well
Solution
I modified my index.html file to have a base of <base href="index.html"> instead of <base href="."> which forced angular to use routes like .../index.html/#/ instead of .../#/.
Non-solution (for future searchers)
I attempted to load the directory and allow the WKWebView to load the index.html page by default by
if let html = Bundle.main.path(forResource: file, ofType: "html", inDirectory: "public"){
let url = URL(fileURLWithPath: html)
let request = URLRequest(url: url).deletingLastPathComponent()
super.load(request)
controller?.view.addSubview(self)
}
However this resulted in the WKWebView not loading the page at all.
I noticed that WKWebView does not load (via loadFileURL:allowingReadAccessToURL:) an NSURL created with fileURLWithPath:relativeToURL:.
I created a github repo showing this behavior: https://github.com/davidkraus/WKWebViewNSURL
As a workaround you can just pass the absolute string of the url to a new NSURL.
var theURL = NSURL(fileURLWithPath: "www/index.html", relativeToURL: folder)
// create a new NSURL
theURL = NSURL(string: theURL.absoluteString)!
webView.loadFileURL(theURL, allowingReadAccessToURL: theURL)
I'm loading a web page using UIWebView
let urlStr = "http://duckduckgo.com"
let urlReq = NSMutableURLRequest(URL: NSURL(string: urlStr)!)
webView.loadRequest(urlReq)
Then, when the page has finished loading, I want to access the html content
func webViewDidFinishLoad(webView: UIWebView) {
let href = webView.stringByEvaluatingJavaScriptFromString("window.location.href")
println("window.location.href = \(href)")
let doc = webView.stringByEvaluatingJavaScriptFromString("document")
println("document = \(doc)")
}
But document just return an empty string (Optional("")). The window.location.href part is working fine. What I'm I doing wrong?
I think you have to evaluate the javascript like this:
let doc = webView.stringByEvaluatingJavaScriptFromString("document.documentElement.outerHTML")
In this case you get the entire HTML.
Now, when UIWebView is deprecated, you'll need to use syntax like this:
webView.evaluateJavaScript("document.documentElement.outerHTML") { (html, error) in
guard let html = html as? String else {
print(error)
return
}
// Here you have HTML of the whole page.
}
After that you can create a function like this that'll easily get HTML from webView:
func getHTML(_ completion: #escaping (String) -> ()) {
webView.evaluateJavaScript("document.documentElement.outerHTML") { (html, error) in
guard let html = html as? String else {
print(error)
return
}
completion(html)
}
}
getHTML { html in
// Here you have HTML string
print(html) // for example...
}
I Found the solution of problem regarding jqxWidget.
That UIWebView content were referencing .js and .css file which is already bundled in App. I had just only add the base url of App's MainBundle.
NSString *html = [self.webview1 stringByEvaluatingJavaScriptFromString: #"document.documentElement.outerHTML"]; //document.body.innerHTML
NSString *path = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithPath:path];
[objFullScreenViewCntrl.webview2 loadHTMLString:html baseURL:baseURL];
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!
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
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)
}
}
}
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.)
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"
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.
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:.
[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]
];
Besides solutions mentioned by Dan Fabulich, XWebView is another workaround. [WKWebView loadFileURL:allowingReadAccessToURL:] is implemented through extension.
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;
}
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];
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)")
}
}
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.
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.
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()
#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:
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
}
Try using
[webView loadHTMLString:htmlFileContent baseURL:baseURL];
Seems it's still working. Yet.