Swift File Download Issue - ios

I am trying to download a plist file from a remote location and use it in the iOS app I am creating. The file is going to be used for calendar details within the app's calendar. The goal is obviously that I can update the remote file instead of having to push updates to the app itself every time we need to make changes to calendar details.
I started with the code used in this example: Download File From A Remote URL
Here is my modified version:
// Create destination URL
let documentsUrl:URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL!
let destinationFileUrl = documentsUrl.appendingPathComponent("2017.plist")
//let destinationFileUrl = URL(string: Bundle.main.path(forResource: String(currentYear), ofType: "plist")!)
//Create URL to the source file you want to download
let fileURL = URL(string: "https://drive.google.com/open?id=0BwHDQFwaL9DuLThNYWwtQ1VXblk")
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL!)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
// Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Successfully downloaded. Status code: \(statusCode)")
}
do {
try FileManager.default.removeItem(at: destinationFileUrl)
try FileManager.default.moveItem(at: tempLocalUrl, to: destinationFileUrl)
print("File was replaced")
print(NSArray(contentsOf: tempLocalUrl))
//print(tempLocalUrl)
} catch (let writeError) {
print("Error creating a file \(String(describing: destinationFileUrl)) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: %#", error?.localizedDescription as Any);
}
}
task.resume()
I originally tried to overwrite the file that is bundled with the app to being with, that resulted in errors. So I instead tried to just save it in the app's documents folder and that removed that error. I had to make sure and remove any previous version of the file because it was giving me a file already exists error after the first run.
While it says everything is working (The outputs for both successful download and replaced file happen) when I print the contents of the array from the downloaded URL it just gives me nil.
This is my first attempt to use any kind of external resources in an app. Before I have always kept everything internal, so I am sure there is something glaringly obvious I am missing.
Update 1:
I realized I didn't have the correct URL to use to download a file from a Google drive. That line of code has been changed to:
let fileURL = URL(string: "https://drive.google.com/uc?export=download&id=0BwHDQFwaL9DuLThNYWwtQ1VXblk")
So now I actually am downloading the plist like I originally thought I was. Even removing the deletion issue mentioned in the first comment, I still can't get the downloaded file to actually replace the existing one.
Update 2:
I have reduced the actual file manipulation down to the following:
do {
try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
print("File was replaced")
print(NSArray(contentsOf: destinationFileUrl))
} catch (let writeError) {
print("Error creating a file \(String(describing: destinationFileUrl)) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: %#", error?.localizedDescription as Any);
}
After the replacement is performed the output of the file shows the correct new contents that were downloaded from the internet.
Later in the code when I try and access the file it seems to be nil in content again.

Look at your download completion code. You:
Delete the file at the destination URL (in case there was one
leftover)
MOVE the temp file to the destination URL (removing it from the temp
URL)
Try to load the file from the temp URL.
What's wrong with this picture?

You are trying to get the contents of the moved file. You already moved the file to destination url and then you are trying to get the contents of the file from temporary location.
For getting file data, Please try the following :
let fileData = try! String(contentsOf: destinationFileUrl, encoding: String.Encoding.utf8)
print(fileData)

Related

Saving to xml document Swift

I'm using this code to save a string to an xml file in my project
let saveTo = statisticsI.toXMLString()
let filepath = Bundle.main.url(forResource: "statistics", withExtension: "xml")
let filepathAlt = Bundle.main.path(forResource: "statistics", ofType: "xml")
print(filepathAlt!)
do {
try saveTo.write(to: filepath!, atomically: false, encoding: String.Encoding.utf8)
let contents = try String(contentsOfFile: filepathAlt!)
print("FILE CONTENTS \(contents)")
}
catch let error as NSError {
print("Error writing values \(error)")
}
Printing the file contents returns the xml correctly, but when I stop running the application, the file hasn't been updated
When the above code is run, the file has already been read by a seperate function. Is the fact that the file has already been accessed (and its path still stored in a variable) the issue?
when I stop running the application, the file hasn't been updated
You can't write into the application bundle in iOS. Try using a path into the Documents folder or some other place within the app's sandbox.

Error when attempting to move item using FileManager

I have a few items that are saved to the documents directory. I currently need those to be moved programmatically to another directory. I created the new directory successfully, but it doesn't seem to be looking at that when using FileManager.default.moveItem.
Code used to created directory.
let path = getDocumentsDirectory().appendingPathComponent("Media").path
do{
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
print("\nDIRECTORY 'Media' CREATED")
}catch {
print("\nDIRECTORY 'Media' WAS NOT ABLE TO BE CREATED")
print("ERROR - \(error)")
}
I use this code to check my URL.
let mediaURL = getDocumentsDirectory().appendingPathComponent("Media")
print("\nMEDIA URL: \(mediaURL)")
Which outputs this.
MEDIA URL:file:///Users/joseph/Library/Developer/CoreSimulator/Devices/552B72BF-8929-40EE-A75B-4574D3D2918A/data/Containers/Data/Application/E168B872-6A1D-4D78-B460-D45194088E5B/Documents/Media/
Here is the code I'm using to move the items.
do{
try FileManager.default.moveItem(at: url, to: mediaURL)
print("\nFILE MOVED TO NEW MEDIA PATH SUCCESSFULLY")
}catch {
print("\nCOULDN'T MOVE FILE TO NEW MEDIA PATH")
print("ERROR - \(error)")
}
Here is the error I'm receiving for each item that I try to move.
COULDN'T MOVE FILE TO NEW MEDIA PATH
ERROR - Error Domain=NSCocoaErrorDomain Code=516 "“image_5” couldn’t be moved to “Documents” because an item with the same name already exists." UserInfo= {NSSourceFilePathErrorKey=/Users/joseph/Library/Developer/CoreSimulator/Devices/552B72BF-8929-40EE-A75B-4574D3D2918A/data/Containers/Data/Application/E168B872-6A1D-4D78-B460-D45194088E5B/Documents/image_5, NSUserStringVariant=(
Move
), NSDestinationFilePath=/Users/joseph/Library/Developer/CoreSimulator/Devices/552B72BF-8929-40EE-A75B-4574D3D2918A/data/Containers/Data/Application/E168B872-6A1D-4D78-B460-D45194088E5B/Documents/Media, NSFilePath=/Users/joseph/Library/Developer/CoreSimulator/Devices/552B72BF-8929-40EE-A75B-4574D3D2918A/data/Containers/Data/Application/E168B872-6A1D-4D78-B460-D45194088E5B/Documents/image_5, NSUnderlyingError=0x60000024fff0 {Error Domain=NSPOSIXErrorDomain Code=17 "File exists"}}
The media directory should be empty, as it is newly created, so the error I'm receiving is a bit confusing. It's also saying it's trying to move it to 'Documents', but it should be moving to 'Media'.What is causing this issue, and how can I resolve it? I'm just trying to move the items saved in 'Documents' to the new directory, 'Media'.
You must append the file name to the destination URL. Per the docs:
The new location for the item in srcURL. The URL in this parameter must not be a file reference URL and must include the name of the file or directory in its new location. This parameter must not be nil.
Delete the old file if exist
let url = NSUrl(string: "...")
if NSFileManager.defaultManager().fileExistsAtPath(url.path!) {
try! NSFileManager.defaultManager().removeItemAtURL(url)
}
Update:
Swift 5.1.2
let url = URL(string: mp3FilePath)
if FileManager.default.fileExists(atPath: url!.path) {
try! FileManager.default.removeItem(at: url!)
}
This worked for me - Add the name of file too in destination path
let nameFile = (songPath as NSString).lastPathComponent
let fileDest = destinationDirectoryPath + "/" + nameFile
//Now move the song files
try fileManager.moveItem(atPath: sourceFilePath, toPath: fileDest)

How to correctly reference/retrieve a temp file created in AppData for file upload to a server?

So the app I'm making creates a file called "logfile" and I'm trying to send that file via Alamofire upload to a server. The file path printed in the console log is
/var/mobile/Containers/Data/Application/3BE13D78-3BF0-4880-A79A-27B488ED9EFE/Documents/logfile.txt
and the file path I can use to manually access the log created in the .xcappdata is
/AppData/Documents/logfile.txt
To access it, I'm using
let fileURL = Bundle.main.url(forResource: "", withExtension: "txt")
where inbetween the double quotes for "forResource", I've tried both file paths I listed in the previous paragraph as well as just the file name but I'm getting a nil value for file found for either. The file isn't recognized to be there, presumably because the file path I'm using is wrong as Alamofire is returning nil when trying to locate send the file. Anyone know the direct file path I'm supposed to use to be able to grab my file since the other two don't supposedly work? Thank you!
Use below code to get string data from text file to upload to server:
let fileName = "logfile"
let documentDirURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = documentDirURL.appendingPathComponent(fileName).appendingPathExtension("txt")
print("FilePath: \(fileURL.path)")
var readString = "" // Used to store the file contents
do {
// Read the file contents
readString = try String(contentsOf: fileURL)
} catch let error as NSError {
print("Failed reading from URL: \(fileURL), Error: " + error.localizedDescription)
}
print("File Text: \(readString)") // Send 'readString' to server
If you're dynamically creating the file at runtime, it won't be in your app bundle so the Bundle class won't be able to find it. The directories you see are also dynamically-generated and not only platform-specific, but also device-specific, so you can't use the file paths directly. Instead, you'll have to ask for the proper directory at runtime from the FileManager class, like this:
guard let documents = FileManager.default.urls(for: .documentsDirectory, in: .userDomainMask).first else{
// This case will likely never happen, but forcing anything in Swift is bad
return
}
let logURL = URL(string: "logfile.txt", relativeTo: documents)
do{
let fileContents = String(contentsOf: logURL)
// Send your file to your sever here
catch{
// Handle any errors you might've encountered
}
Note that I'm guessing based on the paths you pasted in your answer you put it in your application's documents directory. That's a perfectly fine place to put this type of thing, but if I'm wrong and you put it in a different place, you'll have to modify this code to point to the right place

Alamofire: file download and validation failure

In my iOS project I'm using Alamofire library to download remote documents (from a server with Basic Auth) in this way:
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("foo.pdf")
return (filePath.url, [.removePreviousFile, .createIntermediateDirectories])
}
Alamofire.download(myUrlRequest, to: destination).authenticate(user: user, password: password, persistence: .none).validate().response { response in
print(response)
if response.error == nil, let path = response.destinationURL?.path {
print(path)
}
}
This works great! The file is correctly downloaded in the app's Documents folder.
My problem is when user or/and password are wrong. In this case server response status is 401 Unauthorized and .validate() method correctly fails, but in my Documents folder I find the file "foo.pdf" where the content is a xml that explains the 401 error. What I would like is the file saved only if the validate doesn't fail.
My questions: is there a way, with Alamofire, to save the file just in case the response is validated? Or do I have to manually delete the file when validate fails?
I am having the similar issue at the moment. So far, the only thing I could think of is crude
if response.error != nil {
try? FileManager.default.removeItem(at: destinationURL)
}
in response closure.
I will investigate further though.
EDIT 1:
It seems this issue quite some time ago brought this behaviour https://github.com/Alamofire/Alamofire/issues/233

Swift 3.0 FileManager.fileExists(atPath:) always return false

When I use method .fileExists(atPath:)to judge whether the file is exist in file system, the method always return false to me. I checked the file system and the file do exist. Here is my code:
let filePath = url?.path
var isDir : ObjCBool = false
if(self.fileManager.fileExists(atPath: filePath!, isDirectory: &isDir)){
let result = NSData(contentsOfFile: filePath!)
}
or
let filePath = url?.path
if(self.fileManager.fileExists(atPath: filePath!)){
let result = NSData(contentsOfFile: filePath!)
}
the if clause will always be skipped.
I assume your url is an URL type. If so try this out:
let filePath = url?.path // always try to work with URL when accessing Files
if(FileManager.default.fileExists(atPath: filePath!)){ // just use String when you have to check for existence of your file
let result = NSData(contentsOf: url!) // use URL instead of String
}
Saying enough, you should change your implementation like this:
if(FileManager.default.fileExists(atPath: (url?.path)!)){ // just use String when you have to check for existence of your file
let result = NSData(contentsOf: url!) // use URL instead of String
}
EDIT: 1
There is even more better way, you can call it swift-way (:D). You don't have to explicitly check for file existence.
guard let result = NSData(contentsOf: fileURL) else {
// No data in your fileURL. So no data is received. Do your task if you got no data
// Keep in mind that you don't have access to your result here.
// You can return from here.
return
}
// You got your data successfully that was in your fileURL location. Do your task with your result.
// You can have access to your result variable here. You can do further with result constant.
print(result)
Update for Swift 3.0+ without the Objective-Cish NS prefix:
do {
let result = try Data(contentsOf: fileURL)
print(result)
} catch {
print(error)
}
in swift 3
just in case anyone gets confused like i did, here's the full snippets:
let str = "file:///Users/martian2049/Library/Developer/CoreSimulator/Devices/67D744AA-6EEC-4AFD-A840-366F4D78A18C/data/Containers/Data/Application/DD96F423-AF9F-4F4D-B370-94ADE7D6D0A5/Documents/72b8b0fb-7f71-7f31-ac9b-f9cc95dfe90d.mp3"
let url = URL(string: str)
print(url!.path,"\n")
if FileManager.default.fileExists(atPath: url!.path) {
print("FILE Yes AVAILABLE")
} else {
print("FILE NOT AVAILABLE")
}
this prints
/Users/martian2049/Library/Developer/CoreSimulator/Devices/67D744AA-6EEC-4AFD-A840-366F4D78A18C/data/Containers/Data/Application/DD96F423-AF9F-4F4D-B370-94ADE7D6D0A5/Documents/72b8b0fb-7f71-7f31-ac9b-f9cc95dfe90d.mp3
FILE Yes AVAILABLE
notice how the 'file://' got chopped off?
I want to share my experience, in case anyone else gets baffled by this.
Tested on iOS 10-11, Xcode 9.2 and Swift 3.2.
Short answer: if you save a file path to disk, you may solve by not including the Documents directory in it.
Instead, every time you need to retrieve the file with the saved path, get the Documents directory and append the path.
For an iOS app, I was saving an image to .../Documents/Pictures through the relative URL, let's say url.
As the image was saved, a path, let's say url.path, was saved too in a Core Data entity.
When I later tried retrieving the image through FileManager.default.fileExists(atPath: url.path), it always returned false.
I was testing the app on my iPhone. It turned out that, for some reason, every time I ran the app from Xcode, the app identifier folder changed!!
So:
App opened from Xcode -> Image saved -> app closed -> app opened from physical device ->
fileExists -> TRUE
App opened from Xcode -> Image saved -> app closed -> app opened from Xcode -> fileExists -> FALSE
You can check if this is your case by getting and printing the Document folder path (or URL, it doesn't matter) and comparing it with the saved path (or URL). If you get something like this:
/var/mobile/Containers/Data/Application/5D4632AE-C432-4D37-A3F7-ECD05716AD8A/Documents..
/var/mobile/Containers/Data/Application/D09904C3-D80D-48EB-ACFB-1E42D878AFA4/Documents..
you found the issue.
Just use path instead of absoluteString to remove file://
FileManager.default.fileExists(atPath: URL.init(string: "your_url")!.path)
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true);
var path = paths[0] as String;
path = path + "/YourFilePath"
if((NSFileManager.defaultManager().fileExistsAtPath(path))) {
let result = NSData(contentsOfFile: filePath!)}
Try the above code and check again
I had the same problem this worked for me
filePath.replacingOccurrences(of: "file://", with: "")
First, what does your file path looks like? If the path begins with a ~,then it must be expanded with expandingTildeInPath;
Check if the path is inaccessible to your app. iOS App can only visits its sandbox directories.

Resources