Why can't I append a String to a NSURL? - swift

Appending the .txt file component to the URL path doesn't work:
var error:NSError?
let manager = NSFileManager.defaultManager()
let docURL = manager.URLForDirectory(.DocumentDirectory, inDomain:.UserDomainMask, appropriateForURL:nil, create:true, error:&error)
docURL.URLByAppendingPathComponent("/RicFile.txt") <-- doesn't work
via debugger:
file:///Users/Ric/Library/Developer/CoreSimulator/Devices/
<device id>/data/Containers/Data/Application/<app id>/Documents/
Writing a String using docURL to a file doesn't work because of the missing file name.
Reason (via error):
"The operation couldn’t be completed. Is a directory"
So Question: Why doesn't the following work?
docURL.URLByAppendingPathComponent("/RicFile.txt")

URLByAppendingPathComponent: doesn't mutate the existing NSURL, it creates a new one. From the documentation:
URLByAppendingPathComponent: Returns a new URL made by appending a
path component to the original URL.
You'll need to assign the return value of the method to something. For example:
let directoryURL = manager.URLForDirectory(.DocumentDirectory, inDomain:.UserDomainMask, appropriateForURL:nil, create:true, error:&error)
let docURL = directoryURL.URLByAppendingPathComponent("/RicFile.txt")
Even better would be to use NSURL(string:String, relativeTo:NSURL):
let docURL = NSURL(string:"RicFile.txt", relativeTo:directoryURL)

With the update to the Swift language, the suggested call to manager.URLForDirectory(...) no longer works because the call can throw (an exception). The specific error is "Call can throw, but it is not marked with 'try' and the error is not handled". The throw can be handled with the following code:
let directoryURL: NSURL?
do
{
directoryURL = try manager.URLForDirectory(.DocumentationDirectory,
inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
}
catch _
{
print("Error: call to manager.URLForDirectory(...) threw an exception")
}

Related

uncaught exemption when writing to a static dictionary in an HTTP request

I'm getting an Uncaught exemption within a http closure related to a dictionary stating there's an uncaught exemption. When I set a breakpoint exemptions, it points to a dictionary. The dictionary in question is declared in a struct as a static var and has multiple values already in it so how can this be happening? Here's the http request.
session.dataTask(with: request){ (data, response, error) in
if let data = data,
let tile = UIImage(data: data),
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first{
let fileName = Date().timeIntervalSince1970
let filePath = documentsURL.appendingPathComponent(String(describing: fileName))
Maps.tileCachePath[url] = fileName //<- this is where the exception happens
//make sure there is no old file and if so delete it
if FileManager.default.fileExists(atPath: filePath.path){
do {
try FileManager.default.removeItem(at: filePath)
} catch{
print("error deleting old tile")
}
}
//now write the new file
FileManager.default.createFile(atPath: filePath.path, contents: data, attributes: nil)
print(filePath.path)
//return
result(tile, error)
} else {
result(nil, error)
}
}.resume()
It's a typo
Replace
Maps.tileCachePath[url] = fileName
with
Maps.tileCachePath[url] = filePath
Basically Date().timeIntervalSince1970 as a filename is a very bad idea. The number contains fractional seconds which are treated as a file extension.
Use a more reliable file name like a formatted date or at least remove the fractional seconds and add a real file extension.
Date().timeIntervalSince1970
is a double, you might need a string value there.

NSKeyedArchiver.archiveRootObject fails to save

I have a group of custom objects that I'm converting to NSMutableDictionary's, and then creating an array out of them (this part is currently working as expected).
I'm then attempting to save that array of data as a file using NSKeyedArchiver. However, the result of NSKeyedArchiver.archiveRootObject always returns false.
Below is my logic for saving - am I missing something obvious, or perhaps is this the wrong approach? Thank you!
var groupsArray = [Any?]()
for group in file!.groups{
for obj in group.children {
let objDict = obj.convertToDictionary()
groupsArray.append(objDict)
}
}
let documents: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let filePath: String = URL(fileURLWithPath: documents).appendingPathComponent("file.archive").absoluteString
let save: Bool = NSKeyedArchiver.archiveRootObject(groupsArray, toFile: filePath)
EDIT: This also fails if trying to save to the .desktop or the .caches directories.
The issue here is .absoultestring. If the URL object contains a file URL, then we should use .path for working with FileManager or PathUtilities etc. So here replacing .absoultestring with .path will solve the issue
for more details about their difference please refer this answer
try this method to save
1.Method returns filepath.
func filePath(key:String) -> String {
let manager = FileManager.default
let url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
return (url!.appendingPathComponent(key).path)
}
2.Code to save to a file using NSKeyedArchiver.
NSKeyedArchiver.archiveRootObject(groupsArray, toFile: filePath(key: "file.archive"))

FileHandle not accepting my URLs for write access

I'd like to open a uniquely named output file for writing either plist or data, but not having any luck in getting a handle using either URL routine of init(fileURLWithPath:) or init(string:)
func NewFileHandleForWritingFile(path: String, name: String, type: String, outFile: inout String?) -> FileHandle? {
let fm = FileManager.default
var file: String? = nil
var uniqueNum = 0
while true {
let tag = (uniqueNum > 0 ? String(format: "-%d", uniqueNum) : "")
let unique = String(format: "%#%#.%#", name, tag, type)
file = String(format: "%#/%#", path, unique)
if false == fm.fileExists(atPath: file!) { break }
// Try another tag.
uniqueNum += 1;
}
outFile = file!
do {
let fileURL = URL.init(fileURLWithPath: file!)
let fileHandle = try FileHandle.init(forWritingTo: fileURL)
print("\(file!) was opened for writing")
//set the file extension hidden attribute to YES
try fm.setAttributes([FileAttributeKey.extensionHidden: true], ofItemAtPath: file!)
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}
}
debugger shows
which for this URL init routine adds the scheme (file://) but otherwise the same as the other, and I'd like to prefer the newer methods which throw reutrning (-1) when just using paths. The error thrown (2) is an ENOENT (no such entity!?) as I need a handle to write to I'm confused how else to get one? The sample path is a new folder created at desktop to triage.
Unlike the previous answer, I recommend using Data's write(to:options:) API instead of FileManager's createFile(atPath:contents:attributes:), because it is a URL-based API, which is generally to be preferred over path-based ones. The Data method also throws an error instead of just returning false if it fails, so if something goes wrong, you can tell the user why.
try Data().write(to: fileURL, options: [])
I would also suggesting replacing the path-based FileManager.fileExists(atPath:) with the URL-based checkResourceIsReachable():
if false == ((try? fileURL.checkResourceIsReachable()) ?? false)
You can't create a file handle to a non-existent file. That is what is causing the ENOENT error.
Use FileManager createFile(atPath:contents:attributes:) to create the file just before creating the file handle.
do {
fm.createFile(atPath: file!, contents: nil, attributes: [FileAttributeKey.extensionHidden: true])
let fileURL = URL(fileURLWithPath: file!)
let fileHandle = try FileHandle(forWritingTo: fileURL)
print("\(file!) was opened for writing")
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}

FileManager.contentsEqual returns false when comparing copied files

I need to preload SQLite files from my bundle's resources into the application support directory. I want to make sure the correct files are there vs. the empty files that Core Data puts there by default. To do this, I'm using FileManager.default.contentsEqual; however, this always returns false.
I tried testing with a playground, but the copy there is creating alias files, still resulting in a false comparison.
In the app, the files do copy over with the same name and size. The dates are different: the copies have the current date/time rather than the original's timestamps. Using contentsEqual, though, I wouldn't think that matters.
Update: diff at the command line shows the files are the same...
What am I missing?
Here's the code from the playground, which is virtually the same as my app code:
// get the URL for the application support directory
let appSupportDir: URL = try!
FileManager.default.url(for: FileManager.SearchPathDirectory.applicationSupportDirectory,
in: FileManager.SearchPathDomainMask.userDomainMask,
appropriateFor: nil, create: true)
// get the source URLs for the preload files
let sqliteFileBundleURL: URL = Bundle.main.url(forResource: "My_DB", withExtension: "sqlite")!
let sqliteShmFileBundleURL: URL = Bundle.main.url(forResource: "My_DB", withExtension: "sqlite-shm")!
let sqliteWalFileBundleURL: URL = Bundle.main.url(forResource: "My_DB", withExtension: "sqlite-wal")!
// create target URLs for copy to application support directory
let sqliteFileAppSptURL: URL = appSupportDir.appendingPathComponent("My_DB.sqlite")
let sqliteShmFileAppSptURL: URL = appSupportDir.appendingPathComponent("My_DB.sqlite-shm")
let sqliteWalFileAppSptURL: URL = appSupportDir.appendingPathComponent("My_DB.sqlite-wal")
// remove the files if they already exist at the target (for test - app doesn't do this)
do {
let filesFound: [URL] = try FileManager.default.contentsOfDirectory(at: appSupportDir,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
if !filesFound.isEmpty {
for fileURL in filesFound {
try FileManager.default.removeItem(at: fileURL)
}
print("Removed \(filesFound.count) files without error.")
}
}
catch {
print("Error:\n\(error)")
}
// copy the files to the application support directory
do {
try FileManager.default.copyItem(at: sqliteFileBundleURL, to: sqliteFileAppSptURL)
try FileManager.default.copyItem(at: sqliteShmFileBundleURL, to: sqliteShmFileAppSptURL)
try FileManager.default.copyItem(at: sqliteWalFileBundleURL, to: sqliteWalFileAppSptURL)
}
catch {
print("Error: \(error)")
}
// compare the copied target files to their source using contentsEqual
let sqliteFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteFileBundleURL.absoluteString, andPath: sqliteFileAppSptURL.absoluteString)
let sqliteShmFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteShmFileBundleURL.absoluteString, andPath: sqliteShmFileAppSptURL.absoluteString)
let sqliteWalFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteWalFileBundleURL.absoluteString, andPath: sqliteWalFileAppSptURL.absoluteString)
Aha! When using FileManager, one should be using path rather than absoluteString to convert a URL to a String:
// compare the copied target files to their source using contentsEqual
let sqliteFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteFileBundleURL.path, andPath: sqliteFileAppSptURL.path)
let sqliteShmFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteShmFileBundleURL.path, andPath: sqliteShmFileAppSptURL.path)
let sqliteWalFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteWalFileBundleURL.path, andPath: sqliteWalFileAppSptURL.path)
The difference between the two is that path generates a file system-type path:
/var/folders/kb/y2d_vrl133d1b04_5kc3kw880000gn/T/com.apple.dt.Xcode.pg/resources/238FF955-236A-42FC-B6EA-9A74FC52F235/My_DB.sqlite
whereas absoluteString generates a browser-friendly path:
file:///var/folders/kb/y2d_vrl133d1b04_5kc3kw880000gn/T/com.apple.dt.Xcode.pg/resources/238FF955-236A-42FC-B6EA-9A74FC52F235/My_DB.sqlite
Note: path also works in the playground with the alias files.

How to Use SwiftyDropbox's "destination" with a Download

In reviewing the SwiftyDropbox tutorial in the v2 Dropbox API, it shows how to perform a download:
// Download a file
let destination : (NSURL, NSHTTPURLResponse) -> NSURL = { temporaryURL, response in
let fileManager = NSFileManager.defaultManager()
let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
// generate a unique name for this file in case we've seen it before
let UUID = NSUUID().UUIDString
let pathComponent = "\(UUID)-\(response.suggestedFilename!)"
return directoryURL.URLByAppendingPathComponent(pathComponent)
}
client.files.download(path: "/MyFile.db", destination: destination).response { response, error in
if let (metadata, url) = response {
print("*** Download file ***")
let data = NSData(contentsOfURL: url)
print("Downloaded file name: \(metadata.name)")
print("Downloaded file url: \(url)")
print("Downloaded file data: \(data)")
} else {
print(error!)
}
}
I'm unclear what's going on with the destination part. Why do I need to generate a random string for the filename?
When I try to specify my own filename, the download doesn't seem to work:
let destination : (NSURL, NSHTTPURLResponse) -> NSURL = { temporaryURL, response in
let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
return directoryURL.URLByAppendingPathComponent("MyFile.db")
}
I want to download a file from Dropbox named MyFile.db and I want to put it in my device's documents directory with the name MyFile.db and overwrite it if it's already there.
How can I do that?
When you say it doesn't seem to work, I expect you mean you get an error like this:
Error Domain=NSCocoaErrorDomain Code=516 "“CFNetworkDownload_bPYhu1.tmp” couldn’t be moved to “Documents” because an item with the same name already exists." UserInfo={NSSourceFilePathErrorKey=..., NSUserStringVariant=(
Move
), NSDestinationFilePath=..., NSUnderlyingError=0x7fda0a67cea0 {Error Domain=NSPOSIXErrorDomain Code=17 "File exists"}}
SwiftyDropbox, by virtue of using AlamoFire, doesn't currently let you overwrite files using the download function.
Specifically, SwiftyDropbox calls download in AlamoFire, and AlamoFire calls NSFileManager.moveItemAtURL. The documentation for NSFileManager.moveItemAtURL says:
If an item with the same name already exists at dstURL, this method aborts the move attempt and returns an appropriate error.
So, it seems like it's just being cautious, and making it hard for your app to accidentally overwrite (ad potentially lose) data. If you definitely know you want to overwrite a particular file, you'll need to do so explicitly, after the Dropbox API call. We'll consider this a feature request though.
Update: SwiftyDropbox now offers the ability to overwrite the files directly as of version 3.1.0, e.g., using download(path:rev:overwrite:destination:).