I have a project I'm working on that saves data to a PDF. The code for this is:
// Save PDF Data
let recipeItemName = nameTextField.text
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
pdfData.writeToFile("\(documentsPath)/\(recipeFileName).pdf", atomically: true)
I'm able to view the files in a separate UITableView I have in another ViewController. When the user swipes the UITableViewCell I want it to also delete the item from the .DocumentDirectory. My code for the UITableView delete is:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
savedPDFFiles.removeAtIndex(indexPath.row)
// Delete actual row
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
// Deletion code for deleting from .DocumentDirectory here???
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
I've tried finding the answer online but can't find anything for Swift 2. Can someone please help?
I've tried working with this but with no luck:
var fileManager:NSFileManager = NSFileManager.defaultManager()
var error:NSErrorPointer = NSErrorPointer()
fileManager.removeItemAtPath(filePath, error: error)
I just want to remove the particular item swiped and not all data in the DocumentDirectory.
removeItemAtPath:error: is the Objective-C version. For swift, you want removeItemAtPath, like this:
do {
try NSFileManager.defaultManager().removeItemAtPath(path)
} catch {}
In swift, this is a pretty common pattern when working with methods that will throw - prefix the call with try and enclose in do-catch. You will be doing less with error pointers then you would in objective-c. Instead, the errors need to be caught or, as in the snippet above, ignored. To catch and handle the error, you could do your delete like this:
do {
let fileManager = NSFileManager.defaultManager()
let documentDirectoryURLs = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
if let filePath = documentDirectoryURLs.first?.URLByAppendingPathComponent("myFile.pdf").path {
try fileManager.removeItemAtPath(filePath)
}
} catch let error as NSError {
print("ERROR: \(error)")
}
What you want to do is to retrieve the recipeFileName from the edited cell to reconstruct the file path.
It is unclear as to how you are populating your UITableViewCell data, so I will cover the most common scenario.
Assume you have an array of files that you use to populate the dataSource.
let recipeFiles = [RecipeFile]()
with the RecipeFile struct
struct RecipeFile {
var name: String
}
In tableView(_:cellForRowAtIndexPath:), you probably set the recipeFile like so :
cell.recipeFile = recipeFiles[indexPath.row]
so in tableView(_:commitEditingStyle:forRowAtIndexPath:), you can retrieve the file name like this:
let recipeFile = recipeFiles[indexPath.row]
and delete your file
var fileManager:NSFileManager = NSFileManager.defaultManager()
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let filePath = "\(documentsPath)/\(recipeFile.name).pdf"
do {
fileManager.removeItemAtPath(filePath, error: error)
} catch _ {
//catch any errors
}
Related
I have a UITableView set up and its data source is the app's sandbox, so all the rows are filled with the files that were imported using UIDocumentPicker, but I also need to be able to delete those files.
The delete function works, I am able to slide and delete a row and everything, but the file stays in the app's sandbox, so every time I import a new file, the rows are refilled (the TableView reloads every time something is imported) with the previously "deleted" stuff.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCell.EditingStyle.delete {
self.deleteFile()
importedfiles.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
}
}
Plus everything that is in the code already, I need the function to delete the file from the app's sandbox (importedfiles).
Here's what I've got so far, I am able to delete files, but only the entire directory, which is not what I want. Code:
func deleteFile() {
let dirPaths = FM.urls(for: .documentDirectory, in: .userDomainMask)
let docsDir = dirPaths[0].path
if FM.fileExists(atPath: docsDir) {
do {
try FM.removeItem(atPath: docsDir)
} catch {
print("Could not delete file: \(error)")
}
}
}
Edit: "importedfiles" are files that were imported in the app's directory (documents) using UIDocumentPickerViewController. And the TableView uses this data to create cells.
Change your delete method to
func deleteFile(_ url: URL) {
let fm = FileManager.default
do {
try fm.removeItem(at: url)
} catch {
print(error)
}
}
and call it
if editingStyle == UITableViewCell.EditingStyle.delete {
self.deleteFile(importedfiles[indexPath.row])
importedfiles.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
}
This should work but there is no error handling and if you want that you can define your method to return Result
func deleteFile(_ url: URL) -> Result<Void,Error> {
let fm = FileManager.default
do {
try fm.removeItem(at: url)
return .success(())
} catch {
return .failure(error)
}
}
and handle the result in a switch
let result = deleteFile(importedfiles[indexPath.row])
switch result {
case .success:
//Maybe update array and table view here
case .failure(let error):
//dislplay error?
}
i have this code for download PDF file :
var documents = [PDFDocument]()
DispatchQueue.global(qos: .default).async(execute: {
//All stuff here
print("Download PDF");
let url=NSURL(string: urlString);
let urlData=NSData(contentsOf: url! as URL);
if((urlData) != nil)
{
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let fileName = urlString as NSString;
let filePath="\(documentsPath)/\(fileName.lastPathComponent)";
let fileExists = FileManager().fileExists(atPath: filePath)
if(fileExists){
// File is already downloaded
print("PDF Already Downloaded");
}
else{
//download
DispatchQueue.main.async(execute: { () -> Void in
print(filePath)
urlData?.write(toFile: filePath, atomically: true);
print("PDF Saved");
self.refreshData()
})
}
}
})
Now I want to remove this file from uitableview in table and in documentdirecotry how to use index path row and how to find file name for removing
i know i will remove the file here but i don't know how to exactly remove the PDF in documentDirectory and Table
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if (editingStyle == UITableViewCellEditingStyle.delete) {
// handle delete (by removing the data from your array and updating the tableview)
}
}
here is my table view cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! BookshelfCell
let document = documents[indexPath.row]
if let documentAttributes = document.documentAttributes {
if let title = documentAttributes["Title"] as? String {
cell.title = title
}
if let author = documentAttributes["Author"] as? String {
cell.author = author
}
here is my refresh data part
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let contents = try! fileManager.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
documents = contents.flatMap { PDFDocument(url: $0) }
You will need to complete three steps to properly delete the document and update your table view:
Use FileManager's removeItem(at: URL) or removeItem(atPath: String) to delete the file from disk. (note that both of these methods throw so you need to use a do-catch block along with try and only proceed if the method didn't throw an error.) Update: If you look at the documentation for PDFDocument you will find that in addition to the documentAttributes that you are already using there is another optional property, documentURL that should give you exactly what you need to remove it.
Remove the document from documents (you could just refresh the whole array using your existing code but removing a single item is faster). documents.remove(at: indexPath.row)
Finally, you need to tell the table view to remove the row in question (you could of course just reload the whole table view but removing the single cell is cleaner) tableView.deleteRows(at: [indexPath], with .fade)
In case you are unfamiliar with do-catch blocks here is a bit of code from Apple's book on Swift (see below for link) simplified a bit:
do {
try makeASandwich()
eatASandwich() // This only gets called if the line above worked
} catch {
dealWithTheError() // This only gets called if makeASandwich() throws an error
}
Side Note
Apple has a fantastic guide on the Swift Language if you haven't done so yet I suggest reading, at least, The Basics. This will give you a base understanding of the language. If you are also new to programming I would suggest going through Apple's Learn to Code series that is free on iPads in the Swift Playgrounds app. The series will guide you through all the basics of programming giving you the tools to search through the documentation that Apple provides and find answers to your questions.
We all started at the beginning at some point, and we all had to crawl before we could walk and well before we could run.
There is a tableViewController with 5 cells. When I click on a cell, the download starts, if the file is not found.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",indexPath.row))
if fileManager.fileExists(atPath: destinationURLForFile.path){ self)
}else{
var downloadTask: URLSessionDownloadTask!
index = indexPath.row
let url = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
downloadTask = backgroundSession.downloadTask(with: url)
downloadTask.resume()
}}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL){
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",index))
do {
try fileManager.moveItem(at: location, to: destinationURLForFile)
}catch{
print("An error occurred while moving file to destination url")
}
}
The problem is that downloading one of the files stops if I download 2 files at a time. How to fix it?
A couple of thoughts:
A single, numeric index property is obviously insufficient to keep track of the multiple downloads that might be in progress. You need some structure to keep track of the correlation between downloads and their eventual file names in the Documents folder. It might be:
struct Download {
enum Status {
case notStarted
case started
case finished
case failed(Error?)
}
/// URL of resource on web server
let remoteURL: URL
/// URL of file in Documents folder
let localURL: URL
/// The status of the download
var status: Status
}
Now that you have a type to keep track of the state of an individual download, create an array of those Download objects:
var downloads = [Download]()
You might populate that in viewDidLoad, or something like that:
override func viewDidLoad() {
super.viewDidLoad()
// create the `Download` objects, e.g. I'll create one here
let remoteURL = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("file0.pdf")
let status: Download.Status
if try! fileURL.checkResourceIsReachable() {
status = .finished
} else {
status = .notStarted
}
downloads.append(Download(remoteURL: remoteURL, localURL: fileURL, status: status))
// since you're dealing with background session (e.g. tasks may have been previously
// scheduled), let's iterate through any pending tasks, updating status accordingly
backgroundSession.getAllTasks { tasks in
for task in tasks {
guard let index = self.downloads.index(where: { $0.remoteURL == task.originalRequest?.url }) else {
print("cannot find download for \(task.originalRequest?.url)")
return
}
self.downloads[index].status = .started
}
}
}
When the download is done, you can now just look up that download in our array of downloads in order to determine the file URL:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
guard let index = downloads.index(where: { $0.remoteURL == downloadTask.originalRequest?.url }) else {
print("cannot find download for \(downloadTask.originalRequest?.url)")
return
}
do {
try FileManager.default.moveItem(at: location, to: downloads[index].localURL)
downloads[index].status = .finished
} catch {
print("An error occurred while moving file to destination url: \(error.localizedDescription)")
downloads[index].status = .failed(error)
}
}
It's worth noting that the logic that says "if the file doesn't exist, then start download" is, most likely, insufficient. Sure, if the file exists, then the download is done. But what if a download has been started already, but hasn't yet finished? You probably do not want to start a new download if a previously initiated download simply hasn't yet finished.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let index = indexPath.row - 1 // for some reason you're looking at indexes greater than zero, so let's adjust our index for a zero-based index within our array
switch downloads[index].status {
case .notStarted:
let downloadTask = backgroundSession.downloadTask(with: downloads[index].remoteURL)
downloads[index].status = .started
downloadTask.resume()
default:
break
}
}
}
Now, I don't want to get too lost in the details of these code snippets above, but rather I want to make sure you grok the basic concept, namely that you can't have a single numeric index, but rather you need some collection (an array or dictionary) to keep track of all of the various downloads that may be in progress at any given time.
You cannot download two files at a time if you are using single variables (index and downloadTask). Whenever the user selects the second cell, a new value for index is set, so using that value in urlSession:downloadTask:didFinishDownloadingTo: is a mistake when it is being called by the first task.
You need to keep that values in a collection, for example an array of tuples, keeping the index, the task and any other info about the file, for example the file path.
I am trying to set up an app with an In App Purchase that downloads content for 12 levels of a game when that respective pack is purchased.
I am stuck on how to properly move the downloaded images from the cache folder to the Documents folder. Here is my code so far:
func processDownload(sender: NSURL) {
//Convert URL to String, suitable for NSFileManager
var path:String = sender.path!
path = path.stringByAppendingPathComponent("Contents")
//Makes an NSArray with all of the downloaded files
let fileManager = NSFileManager.defaultManager()
var files: NSArray!
do {
files = try fileManager.contentsOfDirectoryAtPath(path)
} catch let err as NSError {
print("Error finding zip URL", err.localizedDescription)
}
//For each file, move it to Library
for file in files {
let pathSource: String = path.stringByAppendingPathComponent(file as! String)
let pathDestination: String = NSSearchPathForDirectoriesInDomains(.LibraryDirectory, .UserDomainMask, true)[0]
//Remove destination files b/c not allowed to overwrite
do {
try fileManager.removeItemAtPath(pathDestination)
}catch let err as NSError {
print("Could not remove file", err.localizedDescription)
}
//Move file
do {
try fileManager.moveItemAtPath(pathSource, toPath: pathDestination)
print("File", file, "Moved")
}catch let err as NSError {
print("Couldn't move file", err.localizedDescription)
}
}
}
Everything actually works just fine except for the errors that are printing from the two do statements. When trying to remove any existing files of the same name in the first do block, I get the following error:
Could not remove file “Library” couldn’t be removed because you don’t have permission to access it.
This subsequently causes the next error from the next do statement to print because the original could not be removed.
Any ideas of why this is happening and how I can properly save the downloaded files elsewhere? Thanks.
I've found a proper working solution. This code will move all of the items in the downloaded zip folder to the Library directory.
func processDownload(sender: NSURL) {
//Convert URL to String, suitable for NSFileManager
var path: String = sender.path!
path = path.stringByAppendingPathComponent("Contents")
//Makes an NSArray with all of the downloaded files
let fileManager = NSFileManager.defaultManager()
var files: NSArray!
do {
files = try fileManager.contentsOfDirectoryAtPath(path)
} catch let err as NSError {
print("Error finding zip URL", err.localizedDescription)
}
//For each file, move it to Library
for file in files {
let currentPath: String = path.stringByAppendingPathComponent(file as! String)
var pathDestination: String = NSSearchPathForDirectoriesInDomains(.LibraryDirectory, .UserDomainMask, true)[0]
pathDestination = pathDestination.stringByAppendingPathComponent(file as! String)
//Move file
do {
try fileManager.moveItemAtPath(currentPath, toPath: pathDestination)
print("File", file, "Moved")
}catch let err as NSError {
print("Couldn't move file", err.localizedDescription)
}
}
}
I can now make SKTextures in SpriteKit with these files like so:
var rippleTex = SKTexture(image: UIImage(contentsOfFile: NSSearchPathForDirectoriesInDomains(.LibraryDirectory, .UserDomainMask, true)[0].stringByAppendingPathComponent("P06_ripple.png"))!)
I am fetching some data from a plist into a UITableview.
I am trying to delete the data selected however, when I try to reload the data to show just the remaining cells the app crashes.
I think the problem is when I use tableview.reloadData() but I am not sure how to fix this problem. If I don't use reloadData the cell will be deleted when I reopen the view controller.
Any Advice?
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
let row = indexPath.row
let plistPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let DocumentsDirectory = plistPath[0] as! String
let path = DocumentsDirectory.stringByAppendingPathComponent("notes.plist")
let fileManager = NSFileManager.defaultManager()
if (!fileManager.fileExistsAtPath(path)) {
if let bundlePath = NSBundle.mainBundle().pathForResource("notes", ofType: "plist") {
let resultDictionary = NSMutableDictionary(contentsOfFile: bundlePath)
println("Bundle notes.plist file is --> \(resultDictionary?.description)")
fileManager.copyItemAtPath(bundlePath, toPath: path, error: nil)
println("copy")
} else {
println("notes.plist not found")
}
} else {
println("note.plist already exists")
//fileManager.removeItemAtPath(path, error: nil)
}
let resultDictionary = NSMutableDictionary(contentsOfFile: path)
//resultDictionary?.removeAllObjects()
resultDictionary?.removeObjectForKey(allKeys[row])
resultDictionary!.writeToFile(path, atomically: false)
println("Loaded notes.plist file is --> \(resultDictionary?.description)")
tableView.reloadData()
}
}
About calling reloadData(), the documentation says: "It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates." so its better to just reload the section where you made the changes and call begin and end if animation is involved
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths(path, withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
and also its preferable to use your own instance of nsfilemanager since the default one works only in the main thread. and also you're unsafely unwrapping resultDictionary when writing to file, that could cause crash
ps,
let path = DocumentDirectory.stringByAppendingPathComponent("notes.plist")
is replaced by stringByAppendingString n swift 2, just fyi