Thursday, December 21, 2017

File Manager sometimes doesn't find subfolder in documents directory

Leave a Comment

I have asked this before, but had to reformulate everything so it becomes more understandable.

In my project, I have created a subfolder inside the documents directory called HTML with the following code:

fileprivate func createFolderOnDocumentsDirectoryIfNotExists() {     let folderName = "HTML"     let fileManager = FileManager.default     if let tDocumentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {         let filePath =  tDocumentDirectory.appendingPathComponent("\(folderName)")         if !fileManager.fileExists(atPath: filePath.path) {             do {                 try fileManager.createDirectory(atPath: filePath.path, withIntermediateDirectories: true, attributes: nil)             } catch {                 print("Couldn't create document directory")             }         }         print("Document directory is \(filePath)")     } } 

The print statement prints the following:

Document directory is file:///var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML

Inside my app's Bundle I have a .css file for me to use in a HTML string to be presented in a WKWebView.

As the .css file needs to be in the same folder as the HTML baseURL , I copy that .css file from the Bundle to the directory created with the above function, with the following code:

fileprivate func copyCSSFileToHTMLFolder() {     guard let cssURL = Bundle.main.url(forResource: "swiss", withExtension: "css") else { return }     let fileManager = FileManager.default     if let tDocumentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {         let fp = tDocumentDirectory.appendingPathComponent("HTML")         if fileManager.fileExists(atPath: fp.path) {             let fp2 =  fp.appendingPathComponent(cssURL.lastPathComponent)             do {                 try fileManager.copyItem(atPath: cssURL.path, toPath: fp2.path)             } catch {                 print("\nFailed to copy css to HTML folder with error:", error)             }         }     } } 

I know I could use both in the app's Bundle by setting the HTML baseURL to the main.Bundle, but since I need to present local images inside the HTML and not web-linked images, I had to move those into a subfolder in order for me to later copy the desired images into that subfolder, since the main.Bundle is read-only and not writable (We cannot save there images).

Now I have a ViewController that gets pushed through a UINavigationController; inside that pushed ViewController I have a WKWebView that will present the HTML.

I also have a var markdownString: String? {} property that once instantiated I convert it to a HTML string using the following function:

func getHTML(str: String) -> String {     /*       the cssImport here points to the previously copied css file as they are on the same folder     */      let cssImport = "<head><link rel=\"stylesheet\" type=\"text/css\" href=\"swiss.css\"></head>"     let htmlString = try? Down(markdownString: str).toHTML()         return cssImport + htmlString! } 

My WKWebView is declared with the following code:

private lazy var webView: WKWebView = {     // script to fit the content with the screen     var scriptContent = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);"      let wkuscript = WKUserScript(source: scriptContent, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)     let wkucontroller = WKUserContentController()     wkucontroller.addUserScript(wkuscript)      let wkwebconfig = WKWebViewConfiguration()     wkwebconfig.userContentController = wkucontroller      let wv = WKWebView(frame: .zero, configuration: wkwebconfig)     wv.frame.size.height = 1     return wv }() 

Whenever my markdown String is set, I convert it to an HTML string, then I instantiate the HTML baseURL and the I load it into my webView with the following code:

public var markdownString: String? {     didSet {         guard let kPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("HTML", isDirectory: true) else { return }          guard let str = markdownString else { return }         let html = getHTML(str: str)          print("base url:", kPath)          let fm = FileManager.default          do {             let items = try fm.contentsOfDirectory(atPath: kPath.path)             print("items:", items)         } catch {             print("Error:", error)         }         webView.loadHTMLString(html, baseURL: kPath)     } } 

Here's where my problem starts.

The swiss.css file is in the following directory:

file:///var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML/

The HTML baseURL points to the following path:

file:///var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML/

As you can see, they're both pointing to the same path.

Sometimes my HTML finds the swiss.css file, as it prints the items in the do{}catch{} block, but other times it doesn't find the folder, the try fm.contentsOfDirectory(atPath: kPath.path) fires an error catched in the catch{} block printing the following:

Error: Error Domain=NSCocoaErrorDomain Code=260 "The folder “HTML” doesn’t exist." UserInfo={NSFilePath=/var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML, NSUserStringVariant=( Folder ), NSUnderlyingError=0x1c044e5e0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}

This both happens in the simulator and on the real device.

What's causing this issue? Why does it finds the folder sometimes and other times it doesn't, even without rebooting the app, which may reset the documents directory path, even though I'm not forcing the path, but instead I'm fetching it as you can see above.

I don't understand this behavior.

UPDATE

So I was thinking that reading the html string directly might be causing some problems so I changed my markdown: String? declaration.

In it, I write the converted markdown string (not an html string) into a file in the same path, called index.html.

Now one think different happened -- when I push this view in the first time, it now finds my swiss.css file and the html file detects it and uses it as expected; so far so good.

but when I dismiss this view controller, poping to the parent view controller (pressing the back button) and then try to push it again, the same error (described previously) prompts:

Failed to write html to file with error: Error Domain=NSCocoaErrorDomain Code=4 "The folder “index.html” doesn’t exist." UserInfo={NSURL=file:///var/mobile/Containers/Data/Application/C0639153-6BA7-40E7-82CF-25BA4CB4A943/Documents/HTML/index.html, NSUserStringVariant=Folder, NSUnderlyingError=0x1c045d850 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}

Note that some of the paths here are different than the previous, because it is a different launch and it changes, but that's not the problem here because I never store the path and force write it, I always fetch the URLs with the default File Manager fetch,

The updated markdown declaration goes by the following:

public var markdownString: String? {     didSet {         guard let kPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("HTML", isDirectory: true) else { return }          guard let str = markdownString else { return }         let html = getHTML(str: str)          do {             let writePath = kPath.appendingPathComponent("index.html")             try html.write(to: writePath, atomically: true, encoding: .utf8)             webView.loadFileURL(writePath, allowingReadAccessTo: kPath)         } catch {             print("Failed to write html to file with error:", error)             return         }          let fm = FileManager.default         do {             let items = try fm.contentsOfDirectory(atPath: kPath.path)             print("items:", items)         } catch {             print("Error:", error)         }        } } 

Somehow this confused me even more.

Why does it works on the first presentation and then it doesn't?

1 Answers

Answers 1

In your UPDATE section you mention the following error:
Failed to write html to file with error: Error Domain=NSCocoaErrorDomain Code=4 "The folder “index.html” doesn’t exist."
As far as I understood your issue, index.html should not be a folder but a file.

I have no idea why it treats index.html as folder, but maybe replacing
let writePath = kPath.appendingPathComponent("index.html")
with
let writePath = kPath.appendingPathComponent("index.html", isDirectory: false)
helps or at least leads to a more specific error message.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment