I am developing a simple app for iOS.
And I was really surprised that when image is written like in the code below, the path stays valid before the app restarts. After the app restarts the new sandbox is created and this invalidates the previous path. I am trying to find a way to have sandbox independent file path. Is it even possible without going through the resolution cycle (i.e. FileManager.default.url...)?
static func saveToDocument(name: String, image: UIImage) -> String? {
guard let data = image.pngData() else { return nil; }
guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) as NSURL else {
return nil
}
do {
let file = directory.appendingPathComponent("\(name).png");
try data.write(to: file!)
return file?.absoluteString
} catch {
return nil
}
}
Stop searching, there is no way, and there is a good reason for that: The safety of your data.
You always have to get the URL of the documents directory with FileManager.default.url(for, it's not that expensive.
Guarding the URL is not necessary, the documents folder is guaranteed to exist.
Return only the file name rather than the complete URL string and I recommend to make the function throw to hand over all errors.
enum FileError : Error {
case noData
}
static func saveToDocument(name: String, image: UIImage) throws -> String {
guard let data = image.pngData() else { throw FileError.noData }
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = directory.appendingPathComponent("\(name).png")
try data.write(to: fileURL)
return fileURL.lastPathComponent
}
And never use NS... classes like NSURL in Swift if there is a native counterpart (URL).
Related
This question already has an answer here:
UIImage(contentsOfFile:) returning nil despite file existing in caches directory [duplicate]
(1 answer)
Closed 1 year ago.
Currently, if I want to create a directory hierarchy in Document directory, I would perform the following
Using NSSearchPathForDirectoriesInDomains (Works fine)
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
let documentUrl1 = URL(string: documentsDirectory)!
//
// /Users/yccheok/Library/Developer/...
//
print("documentUrl1 -> \(documentUrl1)")
let dataPath = documentUrl1.appendingPathComponent("SubFolder1").appendingPathComponent("SubFolder2")
print("dataPath.absoluteString -> \(dataPath.absoluteString)")
if !FileManager.default.fileExists(atPath: dataPath.absoluteString) {
do {
try FileManager.default.createDirectory(atPath: dataPath.absoluteString, withIntermediateDirectories: true, attributes: nil)
print("Folder creation done!")
} catch {
print(error.localizedDescription)
}
}
But, if I use the following
Using FileManager.default.urls (Not working)
let documentUrl0 = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
//
// file:///Users/yccheok/Library/Developer/...
//
print("documentUrl0 -> \(documentUrl0)")
let dataPath = documentUrl0.appendingPathComponent("SubFolder1").appendingPathComponent("SubFolder2")
print("dataPath.absoluteString -> \(dataPath.absoluteString)")
if !FileManager.default.fileExists(atPath: dataPath.absoluteString) {
do {
try FileManager.default.createDirectory(atPath: dataPath.absoluteString, withIntermediateDirectories: true, attributes: nil)
print("Folder creation done!")
} catch {
print(error.localizedDescription)
}
}
The following error will be printed
You can’t save the file “SubFolder2” because the volume is read only.
I was wondering, under what use case, that FileManager.default.urls will be useful? Thanks.
URL(string and absoluteString are the wrong APIs to work with file system paths.
Your code accidentally works because URL(string applied to a path creates a path rather than a valid URL, and absoluteString applied to a path returns the path because there is no scheme (file://).
However you are strongly discouraged from doing that. A file system URL must be created with URL(fileURLWithPath and you can get the path with the path property.
You are encouraged to use the FileManager API because it provides a better error handling and it provides also the URL related API which is preferred over the string path API.
This is the correct FileManager way to create the directory if it doesn't exist
let fm = FileManager.default
do {
let documentUrl = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
//
// file:///Users/yccheok/Library/Developer/...
//
print("documentUrl -> \(documentUrl)")
let dataURL = documentUrl.appendingPathComponent("SubFolder1").appendingPathComponent("SubFolder2")
print("dataPath.path -> \(dataURL.path)")
if !fm.fileExists(atPath: dataURL.path) {
try fm.createDirectory(at: dataURL, withIntermediateDirectories: true, attributes: nil)
print("Folder creation done!")
}
} catch { print(error) }
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!
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) }
I am currently designing a database management application with Realm, where I have managed to create and retrieve an object successfully. The problem I am having is with updating/editing - specifically updating the UIImage that the user has uploaded. With Realm, I save the path of the image and then retrieve it by loading that path (in Documents Directory).
When the user tries to save the changed image, for some odd reason the UIImageJPEGRepresentation saves the changed image as nil, thus removing the user's image. It's strange because the initial creation of a data object stores it just fine.
I have tried to check whether the image is being passed correctly with some debugging, and have found that it does so just fine and the right path is being saved on.
Here is my update method:
func updateImage() {
let documentsDirectoryURL = try! FileManager().url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = documentsDirectoryURL.appendingPathComponent("\(selectedPicPath!)")
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
if profilePic.image != nil {
let image = profilePic.image!.generateJPEGRepresentation()
try! image.write(to: fileURL, options: .atomicWrite)
}
} catch {
print(error)
}
} else {
print("Image Not Added")
}
}
Can anyone see any problems?
let image = profilePic.image!.generateJPEGRepresentation()
Check this line, whether it is returning nil value or data? If nil, then use following code to test your image store, it's working. Also ensure your actual image has JPEG file format extension, that you are trying to generate.
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
// For PNG Image
if let image = UIImage(named: "example.png") {
if let data = UIImagePNGRepresentation() {
let filename = getDocumentsDirectory().appendingPathComponent("copy.png")
try? data.write(to: filename)
}
}
For JPG image
if let image = UIImage(named: "example.jpg") {
if let data = UIImageJPEGRepresentation(image, 1.0) {
let filename = getDocumentsDirectory().appendingPathComponent("copy.jpg")
try? data.write(to: filename)
}
}
I'm making an app in swift 3.0.2 using XCode 8.2 and I am trying to recursively list the files that are shared with the iOS app through itunes.
At the moment, the following code works:
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
print(documentsUrl.absoluteString)
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil, options: [])
print(directoryContents)
} catch let error as NSError {
print(error.localizedDescription)
}
However, the documentation for contentsOfDirectory located here states that the function only performs a shallow traversal of the URL and suggests a function that does a deep traversal of the URL, namely enumerator, whose documentation is located here.
I am using the following snippet to try and list all files under the current URL using deep traversal:
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
print(documentsUrl.absoluteString)
let dirContents = FileManager.default.enumerator(at: documentsUrl.resolvingSymlinksInPath(), includingPropertiesForKeys: nil, options: [])
print(dirContents.debugDescription)
while let element = dirContents?.nextObject() as? String {
print(element)
}
The issue is that while the first snippet does display file URLs the second one does not display anything.
Could somebody please advise on what could I do to fix that? I would really like to use the second snippet rather than attempt a workaround using the first function.
FileManager has two methods to obtain a directory enumerator:
enumerator(atPath path: String)
enumerator(at url: URL, ...)
The first one returns an enumerator which enumerates strings
(the file paths), the second one returns an enumerator which
enumerates URLs.
Your code uses the URL-based enumerator, therefore the conditional
casts to as? String fail and no output is produced.
You have to cast to URL instead:
if let dirContents = FileManager.default.enumerator(at: documentsUrl.resolvingSymlinksInPath(), includingPropertiesForKeys: nil) {
while let url = dirContents.nextObject() as? URL {
print(url.path)
}
}
You can also iterate with a for-loop:
if let dirContents = FileManager.default.enumerator(at: documentsUrl.resolvingSymlinksInPath(), includingPropertiesForKeys: nil) {
for case let url as URL in dirContents {
print(url.path)
}
}
we can iterate over all URLs with an enumerator and return valid file URLs using the below method
func extractFileURLs(from directory: String = "JsonData", withExtension fileExtension: String = "json") -> [URL] {
var jsonFileURLs: [URL] = []
if let enumerator = FileManager.default.enumerator(
at: Bundle.main.bundleURL, // replace url if using another bundle
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) {
for case let fileURL as URL in enumerator where fileURL.pathExtension == fileExtension {
jsonFileURLs.append(fileURL)
}
}
guard !jsonFileURLs.isEmpty else {
assertionFailure("Please verify directory name or file extension")
}
return jsonFileURLs
}