File Manager sometimes doesn't find subfolder in documents directory - ios

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?

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.

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.

Related

Unable to write an encoded json Data object to a local file in Swift 5

I have a data.json file in the root of my Swift 5 project to which I would like to write some data. The file is already filled with data beforehand, but I would like to overwrite it.
This is the function I use to encode an array of Task structs into JSON Data:
func encode(task: Task){
loadJSON()
t.append(task)
if let json = try? JSONEncoder().encode(t){
saveJSON(json: json)
}
}
loadJSON() loads the JSON data into the t array from the data.json file, then the new Task is appended to the array, and then I create a json constant with encoded data from the t array, and call the saveJSON function.
func saveJSON(json: Data){
if let path = Bundle.main.path(forResource: "data", ofType: "json"){
let url = URL(fileURLWithPath: path)
do{
try json.write(to: url, options: .atomicWrite)
}
catch{
print(error)
}
}
}
The path and the url are set, then the do is executed and it just falls through without doing anything. The data.json file is unchanged and json.write doesn't throw any errors.
I'm not entirely sure why am I unable to write to the file. I've tried to write a simple string instead of a data set, with similar results.
The problem seems to be caused by your attempting to write to the bundle directory, which cannot be written to directly (from all accounts, it seems). Moving your file to a writable directory, like the user's documents directory, will allow you to modify or delete the file as you wish.
You can copy your resource file into the documents directory so it can be accessed locally, and made available to the user as well. Here's an example helper method that copies a file (in your case, pass the string data.json as the sourceFile argument) from the Bundle into the documents directory:
func copyFileFromBundleToDocumentsFolder(sourceFile: String, destinationFile: String = "") {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
if let documentsURL = documentsURL {
let sourceURL = Bundle.main.bundleURL.appendingPathComponent(sourceFile)
// Use the same filename if destination filename is not specified
let destURL = documentsURL.appendingPathComponent(!destinationFile.isEmpty ? destinationFile : sourceFile)
do {
try FileManager.default.removeItem(at: destURL)
print("Removed existing file at destination")
} catch (let error) {
print(error)
}
do {
try FileManager.default.copyItem(at: sourceURL, to: destURL)
print("\(sourceFile) was copied successfully.")
} catch (let error) {
print(error)
}
}
}
After doing that -- if you need to make modifications to the newly created file, you can create another function that overwrites the file with your specific json data using almost exactly the same logic.
You can adapt your loadJSON() and saveJSON() functions to read from or write to this new file (inside the document directory). For saving, something like this:
func saveJSONDataToFile(json: Data, fileName: String) {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
if let documentsURL = documentsURL {
let fileURL = documentsURL.appendingPathComponent(fileName)
do {
try json.write(to: fileURL, options: .atomicWrite)
} catch {
print(error)
}
}
}
EDIT:
Changes to the Document Directory should persist. Here is code to retrieve the list of all files in that directory. It should help to check that the file in question is being persisted properly:
func listDocumentDirectoryFiles() {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
if let url = documentsURL {
do {
let contents = try FileManager.default.contentsOfDirectory(atPath: url.path)
print("\(contents.count) files inside the document directory:")
for file in contents {
print(file)
}
} catch {
print("Could not retrieve contents of the document directory.")
}
}
}
Hope this helps. Feel free to ask in the comments if anything is unclear. Good luck!

display in a textView a .log file from a filePath

I'm trying to display a .log file in a textview but can't seem to access the filepath properly. When I do it returns nil. I'm using a pod called "SwiftyBeaver" to do the logging. This is what the fileURL looks like:
file:///var/mobile/Containers/Data/Application/5C92E3E6-9E45-4869-9142-AB9E70EE4FCC/Library/Caches/swiftybeaver.log
This is the function I'm using to turn the .log into a string so I can display it in a textView
private func loadTextWithFileName(_ fileName: String) -> String? {
guard let path = Bundle.main.path(forResource: fileName, ofType: "log"),
let contents = try? String(contentsOfFile: path) else {return nil}
return contents
}
This is how I'm displaying the text to the textView
self.loggingTextView.text =
self.loadTextWithFileName(self.file.logFileURL!.absoluteString)
The method that you are using Bundle.main.path() is mainly to search for files in your Bundle.
But it seems your log file is going to be in your Cache directory.
Here is how you can look for a file in your Cache directory of your app
private func loadTextWithFileName(_ fileName: String) -> String? {
if let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
let fileURL = dir.appendingPathComponent(fileName)
guard let text = try? String(contentsOf: fileURL, encoding: .utf8) else {
return nil
}
return text
}
return nil
}
You can add a catch block to your try case to check what the error is, in case you don't get the log file details.

Swift 4.0 - Unable to create empty text file in iOS

Below is my code -
I have tried to get the document directory path and with standard FileManager singleton tried to create a file, but I am not able to create the file, as the error -
Unable to store data: Error Domain=NSCocoaErrorDomain Code=4 "The file “CrashLog.txt” doesn’t exist."
UserInfo={NSFilePath=file:///Users/ABC/Library/Developer/CoreSimulator/Devices/87317777-63E7-422B-A55F-878E3267AFB8/data/Containers/Data/Application/4B41AA87-E4B9-4EE4-A67F-AC3B018913CC/Documents/CrashLog,
NSUnderlyingError=0x600000244ec0 {Error Domain=NSPOSIXErrorDomain
Code=2 "No such file or directory"}}
Code in development -
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
if (paths.count > 0) {
let documentsDirectory = paths[0]
let logFilePath = URL(fileURLWithPath: documentsDirectory).appendingPathComponent("CrashLog.txt").absoluteString
let _string = "Hello"
//Create file at given path
let data = _string.data(using: .utf8)
//let attributes = FileManager.default.attributesOfItem(atPath: logFilePath)
let fileExists : Bool = FileManager.default.fileExists(atPath: logFilePath)
print(fileExists)
let isFileCreated = FileManager.default.createFile(atPath: logFilePath, contents: data, attributes: nil)
print("ifFileCreated", isFileCreated)
}
Here's my take on what you've done. Adopt the URL-based means of working with files. The best way to write data (for this example, at least), is to use Data's ability (not FileManager) to write to a file, again, using a URL. In most cases, you don't need to worry whether the file exists or not; just do it, and handle any error that arises.
if var url = try? FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false) {
url = url.appendingPathComponent("CrashLog").appendingPathExtension("txt")
let _string = "Hello"
if let data = _string.data(using: .utf8) {
do {
try data.write(to: url)
print("successful")
} catch {
print("unsuccessful")
}
}
}
The appendingPathComponent method if the receiver (e.g. parameter) does not end with a trailing slash, then it may read file metadata to determine whether the resulting path is a directory. That means it may produce the error you are seeing, so better use the appendingPathComponent(_:isDirectory:) instead.
For example:
let logFilePath = URL(fileURLWithPath: documentsDirectory).appendingPathComponent("CrashLog.txt", isDirectory: false).absoluteString
The API absoluteString is wrong. The correct API is path
absoluteString returns the entire URL string representation including the scheme file://. On the other hand the path API of FileManager expects file system paths, the string without the scheme.
You are encouraged to use the URL related API anyway and you can write Data directly to disk without explicitly creating a file.
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let logFileURL = documentsURL.appendingPathComponent("CrashLog.txt")
let string = "Hello"
let data = Data(string.utf8)
let fileExists = FileManager.default.fileExists(atPath: logFileURL.path)
print(fileExists)
do {
try data.write(to: logFileURL)
print("data written")
} catch { print(error) }

Download and view document in ios from firebase storage

I have a firebase storage link of a document which I want to download in my ios device's Document directory and then view it from there using QuickLook Module.
I am able to download document in iOS device but it is not saving with the name i am providing.
I want to create following directory structure and then need to save document in "doc" folder.
Documents/chat/doc
following code is to create "/chat/doc" inside Iphone's Document directory. Where folderName will contain "/chat/doc".
func createDocument(folderName: String ,completion: #escaping(Bool,String)->()){
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
if let documentsDirectory = path{
//
let docDirectoryPath = documentsDirectory.appending(folderName)
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: docDirectoryPath) {
do {
try fileManager.createDirectory(atPath: docDirectoryPath,
withIntermediateDirectories: true,
attributes: nil)
//return path on success
completion(true,docDirectoryPath)
return
} catch {
print("Error creating folder in documents dir: \(error.localizedDescription)")
completion(false,error.localizedDescription)
return
}
}
completion(true,docDirectoryPath)
}
}
after creating folder i want to download document and need to store in newly created folder in Document Directory.
Following code will download document.
func downloadDocument(){
createDocument(folderName: "/chat/doc") { (status, path) in
if let docURL = self.documentURL{
if status{
//folder is created now download document
let docDownloadRef = Storage.storage().reference(forURL: docURL)
//get documents metadata from firebase to get document name
docDownloadRef.getMetadata { metadata, error in
if let error = error {
// Uh-oh, an error occurred!
} else {
//get document name from metadata
if let fileName = metadata?.name{
//create file system url to store document
let docsurl = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let myurl = docsurl.appendingPathComponent("chat/\(fileName)")
_ = docDownloadRef.write(toFile: myurl) { url, error in
if let error = error {
print(error.localizedDescription)
} else {
// Local file URL for document is returned
print("doc url \(url?.absoluteString)")
}
}
}
}
}
}else{
//folder is not created show document using online location
}
}
}
}
Ultimate goal is to achieve something like this "Documents/chat/doc/abc.doc"
but my code is not storing document using file name it stores like this "Documents/chat/doc" where doc is considered as a document not a folder.
Please help to let me know what I am doing wrong.
Thank you.
When you are creating url at below line, here you are missing the path for doc folder.
let myurl = docsurl.appendingPathComponent("chat/\(fileName)")
This will save to chat folder. You need to pass the same folder while creating i.e folderName: "/chat/doc" so it should be
let myurl = docsurl.appendingPathComponent("chat/doc/\(fileName)")

Why are deleted files coming back after a new write to file? Swift 2.0

I am writing an app in swift that logs sensor data to a txt file. When I have an event occur that needs to be logged I create the filename
func createNewLogFile (){
// Create a new file name
currentFileName = "log\(NSDate()).txt"
//get the path
let paths = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
//create the file
_ = paths[0].URLByAppendingPathComponent(currentFileName)
}
After the file is created I write data to the new file like this:
func writeData (data: String){
// get the path to document directory
let paths = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let filePath = paths[0].URLByAppendingPathComponent(currentFileName)
//get the data to be logged
let stringLocation = data
let stringData = stringLocation.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
//look to see if the file exist
if NSFileManager.defaultManager().fileExistsAtPath(filePath.path!) {
do {
//seek to the end of the file to append data
let fileHandle = try NSFileHandle(forWritingToURL: filePath)
fileHandle.seekToEndOfFile()
fileHandle.writeData(stringData)
fileHandle.closeFile()
} catch {
print("Can't open fileHandle \(error)")
}
} else {
do {
// write to new file
try stringData.writeToURL(filePath, options: .DataWritingAtomic)
} catch {
print("Can't write to new file \(error)")
}
}
}
When I delete the files (from a different ViewController or the same, I tried both)
I am calling this DeleteAllFiles
func deleteAllFiles (Extension: String){
let dirs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let dir = dirs[0]
do {
let fileList = try NSFileManager.defaultManager().contentsOfDirectoryAtURL(dir, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions())
//return fileList as [String]
for elements in fileList{
do{
try NSFileManager.defaultManager().removeItemAtURL(elements)
print("old Files has been removed")
} catch let error as NSError {
print(error.localizedDescription)
}
}
}catch let error as NSError {
print(error.localizedDescription)
}
}
I then refresh the list and the files seem to be gone.(even when I go back and forth between views) However, when I write a new file and refresh the list the files are back with the new file.
This even happens when I delete them from iTunes using the shared files feature.
Any ideas on why this is happening? I am not getting any helpful error messages.
I found the fix for the problem.
When I was creating the file I actually only meant to create the file name. There was no reason to actually create the file at this time. I am creating the actual file when I write to it.
func createNewLogFile (){
// Create a new file name
currentFileName = "log\(NSDate()).txt"
//Removed creating actual file code
}

Resources