Why does "Show" Segue Update UITableViewCells but not reloadData()? - swift

I have a problem which iv been dealing with for a long time just never got around to seeing why.
Problem
I have a UItableview and load data into it from Firebase. When I segue "show" from ViewController #1 to ViewController #2 (with the UITableView) the firebase data is loaded into the cells.
Each cell has a UITableViewRowAction "delete" which basically deletes the Firebase information. But when i press the button..it does delete the Firebase information but not the cell. In order for that UITableView to update I have to segue "show" back to any other ViewController and come back to ViewController #2 for it to update.
** This method only updates if there is more than 1 UITableViewCell in the table
Problem #2 - Only 1 UITableViewCell
Now if there is only 1 UITableViewCell loaded and I "delete" it....it never goes away unless I create a new UITableViewCell in ViewController #2.
What have i tried?
self.tableView.endUpdates()
self.tableView.reloadData()
Iv literally plugged reloadData() anywhere I could to see if would reload data but it just wont reload it!
let delete = UITableViewRowAction(style: .Default, title: "Delete", handler: { (action, indexPath) in
self.tableView.dataSource?.tableView?(
self.tableView,
commitEditingStyle: .Delete,
forRowAtIndexPath: indexPath
)
self.tableView.beginUpdates()
let uid = NSUserDefaults.standardUserDefaults().valueForKey("uid") as! String // uid
DataService.dataService.deleteAudio(uid, project: self.sharedRec.projectName, vocalString: self.sharedRec.audioString )
let currURL = self.sharedRec.audioString
let url = "\(currURL).caf"
//Document Directory
let dirPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
// Full File to Delete Location
let fileURL = dirPath.stringByAppendingPathComponent(url)
// This is to print out all directory files
let documentsUrl = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
do {
tableView.beginUpdates()
print("DELETED LOCAL RECORD")
try NSFileManager.defaultManager().removeItemAtPath(fileURL)
print("file deleted \(fileURL)")
let directoryContents = try NSFileManager.defaultManager().contentsOfDirectoryAtURL(documentsUrl, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions())
print("PRINT LOCAL DIC\(directoryContents)") // NOT WORKING
self.tableView.reloadData()
} catch {
print("error")
}
tableView.deleteRowsAtIndexPaths([NSArray arrayWithObject: indexPath], withRowAnimation: .Automatic) // SHOWS ERROR
self.tableView.endUpdates()
return
})

In function commitEditingStyle delete data from your array, database etc.
Decrement your current row count.
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.endUpdates()

Related

Get trouble of updating tableview rows when deleting Firebase data

something goes wrong when trying to update rows of tableview after delete of Firebase data.
Below is method I use.
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
let cell = self.messages[indexPath.row]
let b = cell.msgNo
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
action.queryOrdered(byChild: "msgNo").queryEqual(toValue: b).observe(.childAdded, with: { snapshot in
if snapshot.exists() { let a = snapshot.value as? [String: AnyObject]
let autoId = a?["autoID"]
action.child(autoId as! String).removeValue()
self.messages.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
} else {
print("snapshot empty")
}}) }
...
return [delete, edit, preview]
}
Initially I checked whole logic without including line /*action.child(autoId as! String).removeValue()*/ then it works normally and removes rows as should be. But once I add this line it removes data from Firebase but tableview is updated in strange way by adding new rows below existing
My guess is that somewhere else in your application you have code like action .observe(.value, which shows the data in the table view. When you delete a node from the database, the code that populates the database gets triggered again, and it adds the same data (minus the node that you removed) to the table view again.
When working with Firebase it's best to follow the command query responsibility segregation principle, meaning that you keep the code that modifies the data completely separate from the flow that displays the data. That means that your code that deletes the data, should not try to update the table view. So something more like:
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
action.queryOrdered(byChild: "msgNo").queryEqual(toValue: b).observe(.childAdded, with: { snapshot in
if snapshot.exists() { let a = snapshot.value as? [String: AnyObject]
let autoId = a?["autoID"]
action.child(autoId as! String).removeValue()
} else {
print("snapshot empty")
}}) }
All the above does is remove the selected message from the database.
Now you can focus on your observer, and ensuring it only shows the messages once. There are two options for this:
Always clear self.messages when your .value completion handler gets called before you add the messages from the database. This is by far the simplest method, but may cause some flicker if you're showing a lot of data.
Listen to the more granular messages like .childAdded and .childRemoved and update self.messages based on those. This is more work in your code, but will result in a smoother UI when there are many messages.

UITableView - Deleting rows with animation

I'm trying to delete row from tableview using animation however in most cases it gets stuck. 1 in 15 tries will result in this animation being played. This is what my delete action looks like:
func contextualDeleteAction(forRowAtIndexPath indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .destructive,
title: "Delete") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
Model().context.delete(notesArray[indexPath.row])
notesArray.remove(at: indexPath.row)
self.myTableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.bottom)
Model().saveItems()
}
action.title = "Delete"
return action
}
This is what it looks like when it gets stucked when swiping.
When pressing delete button instead.
I've also tried to use tableView.beginUpdate() and tableView.endUpdate() but didn't get different result.
The problem is that you have forgotten your primary duty in this method: you must call the completion handler!
Model().context.delete(notesArray[indexPath.row])
notesArray.remove(at: indexPath.row)
self.myTableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.bottom)
Model().saveItems()
completionHandler(true) // <-- look!
In Swift 4, you can perform a delete animation just like the iPhone Messages animation.
Use this:
tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic)

swift UICollectionView showing empty sometimes bug even header

I have a UIcollectionView that has a header and Footer and sometimes a weird bug just shows the screen empty. It is strange because even on the View Debugger it shows as empty but the Header is a static image that is on the app not something is getting from API and that does not show either.
Also the console does not give any errors methods. I try going to another view and forcing a reloadData() but still does not show anything. Any way I can debug this better or make sure it does not happen?
This is how the view debugger looks like:
You can see the header and footer empty reusable views:
Edit:
This is how the supplementary views are being created using RxDataSources
dataSource.supplementaryViewFactory = { (dataSource, collectionView, kind, indexPath) in
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CellReuseId.contentHeaderCollectionView, for: indexPath ) as! ContentHeaderView
headerView.imageView.image = ImageAssets.contentBanner
let tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(self.showListOfEvents(_:)))
headerView.addGestureRecognizer(tapGestureRecognizer)
return headerView
case UICollectionElementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CellReuseId.contentFooterCollectionView, for: indexPath) as! ContentFooterView
footerView.setUpObjects()
//setUp Appropiate label or animation
let showFooter: Bool = !(self.centerActivityIndicator?.isAnimating ?? false)
footerView.setUpAppropiateDependingOnResults(isThereMoreResults: self.isThereMoreResults, showFooter: showFooter)
return footerView
default: break
}
//return default
return UICollectionReusableView()
}
And this is the code that gets the models for the API and binds them to the collectionView
let results = Observable.of(paginationObserver, offlineObserver).merge()
//calls method that calls DB with the appropiate data
.flatMapLatest { [unowned self] parametersChanged -> Driver<LecturesStateResults> in
//since this is being called again, we make sure to clean out "old cache data" on view model
self.videoObject.lecturesResults.value.removeAll(keepingCapacity: true)
return self.setupLectures().asDriver(onErrorJustReturn: LecturesStateResults.empty)
}
results
//Bind the result observable to the UIcollectionView
//UiCollection view only wants an array not an Observable
.map {
if !$0.results.isEmpty { self.centerActivityIndicator?.stopAnimating()}
return [SectionModel(model: "", items: $0.results)]
}
.bind(to: lectureViewSquare.rx.items(dataSource: dataSource))
.addDisposableTo(disposeBag)
lectureViewSquare.rx.setDelegate(self)
.addDisposableTo(disposeBag)
I had the same problem with the same ui rendering, but without using RxSwift.
The code that fixed the issue was
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});

How can I implement swipe to delete for UITableView containing filtered data from Core Data?

I have two VC with table views, the first showing categories, and the second showing items of selected category (recipes). I am able to get the RecipeTableVC to display filtered data using NSPredicate, but I haven't quite figured out how to delete the recipe from Core Data since the data displayed is a variable containing only the predicated data.
Here is my fetch:
func attemptRecipeFetch() {
let fetchRecipeRequest = NSFetchRequest(entityName: "Recipe")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRecipeRequest.sortDescriptors = [sortDescriptor]
let controller = NSFetchedResultsController(fetchRequest: fetchRecipeRequest, managedObjectContext: ad.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedRecipeController = controller
do {
try self.fetchedRecipeController.performFetch()
let allRecipes = fetchedRecipeController.fetchedObjects as! [Recipe]
recipesOfCategory = allRecipes.filter { NSPredicate(format: "category = %#", selectedCategory!).evaluateWithObject($0) }
} catch {
let error = error as NSError
print("\(error), \(error.userInfo)")
}
}
So what's populating my table is the recipesOfCategory array.
Here is my attempt to delete so far:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
recipesOfCategory.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
ad.managedObjectContext.delete(recipesOfCategory[indexPath.row])
}
}
This crashes and I understand why, but still haven't come up with a solution. Is there a way to implement swipe to delete where it deletes the recipe from Core Data? Am I using the correct methodology to populate the table with filtered data?
I used the following code to 'Swipe to delete from core data' in a table view for an App I recently did. I may work for you.
In your "tableView:commitEditingStyle",
1. set up CoreData access with ...
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext = appDel.managedObjectContext
2. Delete the desired row + incl. from Core Data...
if editingStyle == UITableViewCellEditingStyle.Delete {
context.deleteObject(self.resultsList[indexPath.row]) // Always before
as CoreD
self.resultsList.removeAtIndex(indexPath.row)
do {
try context.save()
} catch {
print("Error unable to save Deletion")
}
} // end IF EditingStyle
self.tableView.reloadData()
In your tableView:commitEditingStyle: you need to just delete the underlying object from Core Data, not from the table view. The NSFetchedResultsController delegate methods will tell you when to remove it from the table view.

iOS UIImageView not refreshing automatically

I'm learning iOS programming by following this wonderful tutorial, except I'm targeting iOS 9 and make some small modifications.
In the tableView() function below, I can get thumbnail image downloaded and my handlers invoked, as evident from the console print out of the two logging lines. However, when the app is running (in simulator), I have to click on each table cell to get the image to show up. I tried to see if there is a refresh() or something like that in the UIImageView or the table cell, but found nothing.
How to make the image show up immediately as the data is received?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier)!
let album = self.albums[indexPath.row]
// ... setting up other cell's data, see the tutorial link
// Start by setting the cell's image to a static file
// Without this, we will end up without an image view!
cell.imageView?.image = UIImage(named: "Blank52")
let request: NSURLRequest = NSURLRequest(URL: thumbnailURL)
let urlSession = NSURLSession.sharedSession()
urlSession.dataTaskWithRequest(request, completionHandler: {(data, response, error) -> Void in
print("received thumbnail \(thumbnailURLString)") // reached
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
}
}).resume()
return cell
}
This part doesn't make sense:
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
This seems to be creating an infinite loop. Every call to cellForRowAtIndexPath will cause another call to cellForRowAtIndexPath.
I think you just meant:
cell.imageView?.image = image
You already know the cell. You just created it before calling this block. You don't need to look it up again.
I figured out a work around. If I also update the text slight, the image shows up right the way without clicking on the table view cell.
cellToUpdate.imageView?.image = image
cellToUpdate.textLabel?.text = "\(album.title) " // a space is added to the end to make a difference and force the cell to be updated
Sounds like a bug in UITableViewCell (or the simulator? haven't tried on a real device) to me.
Why u just try without dispatch_async(dispatch_get_main_queue()) part.
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
to
cell.imageView?.image = image
You probably need to cast the cell when you get the cell you try to update.
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) as! UITableViewCell {
cellToUpdate.imageView?.image = image
}
})
Swift 5 Just put the following piece of code in the cellForRowAt delegate method after setting the image.
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
// Full Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as? PhotoCell else { return UITableViewCell() }
let model = self.arrPhotos?[indexPath.row]
// Below method download image from url using asycn
SwiftHelper.downloadImageFrom(strUrl: model?.url ?? "") {[weak self] (img) in
guard let _ = self, let image = img else { return }
print("ImgDownloaded")
cell.imgView.image = image
// cell.layoutIfNeeded()
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
}
return cell
}
To do animation on image you can remove performWithoutAnimation block.