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?
}
Related
My app saved photo to the local documents folder and I used the UICollectionView to display all the image from that folder. But whenever I try to open the CollectionView it often took several seconds to open. I'm thinking that maybe the image files are too big, each photo is around 10MB. I also tried using thumbnails to display in collectionview but it still too slow. Any idea how to speed that up?
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SPCell
// Configure the cell
cell.imageView.image = loadImage(fileName: self.fileURLs[indexPath.row])
return cell
}
func loadImagesFromDocuments(){
let fileManager = FileManager.default
let documentsURL = NSHomeDirectory() + "/Documents/Secure/"
do {
fileURLs = try fileManager.contentsOfDirectory(at: URL(string: documentsURL)!, includingPropertiesForKeys: nil)
} catch {
print("Error while enumerating files : \(error.localizedDescription)")
}
}
func loadImage(fileName: URL) -> UIImage? {
do {
let imageData = try Data(contentsOf: fileName)
return UIImage(data: imageData)
} catch {
print("Error loading image : \(error)")
}
return nil
}
Current problem is that you load the image every time the cell appears so Instead of
var fileURLs = [URL]()
Make it
var fileImages = [UIImage]()
Then inside viewDidLoad
fileImages = fileURLs.compactMap { self.loadImage(fileName: $0) }
you are synchronously loading images while returning every cell at indexPath.
cell.imageView.image = loadImage(fileName: self.fileURLs[indexPath.row])
Instead, you can create a custom UICollectionViewCell implementation with a variable in global scope say imageURL.
after cell initialization, do this:
cell.imageURL = self.fileURLs[indexPath.row]
and, in ViewDidLoad() of your custom class, add this line:
self.imageView.image = loadImage(fileName: self.imageURL)
doing so, will lead to images being loaded in each custom implementation of cell unblocking the thread which is invoking the DataSource of your CollectionView.
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 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
}
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