Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

File Manager sometimes doesn't find subfolder in documents directory

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?

like image 775
Ivan Cantarino Avatar asked Dec 12 '17 19:12

Ivan Cantarino


1 Answers

The error is telling you that it can't write to that file path, which means it doesn't exist. You should confirm this in your markdownString method by adding a check for FileManager.default.fileExists(atPath: kPath.path) before you try to write index.html to the writePath.

Once you confirm that, you need to track the life-cycle of the HTML folder's creation (just keep checking FileManager.default.fileExists(atPath: thePath) until you track down when it's disappearing/not getting created.

From the code you posted, there's no obvious indication why this is happening. It has to be somewhere else in your app.

like image 54
Nima Yousefi Avatar answered Oct 23 '22 08:10

Nima Yousefi