I have a list of favorite movies. In the table cell, there is a button to delete the movie from the favorites list. So I want to animate it with tableView.deleteRows. But if I delete a row from the top of the screen the other rows coming upward. It is normal but when I was deleting the row from the top of the screen other cells that already shown in the list are not updating indexPath. I assign an asyncAfter method for tableView.reloadData to solve the problem but I think that can cause a crash on the OS side. Because I forced to main thread reload data after a delay. Is it really a problem for OS and what should I do?
Example problem:
If I delete the movie which has the 0 index all of the movies coming upward. So before deletion, 1 index movie should has 0 index but it not. If I tried to delete a new 0 index movie it is deleting another movie.
Work example without tableView.reloadData()
Here is my code // This is working but I think asyncAfter is a problem
cell.deleteButtonActionBlock = {
FavouritesHandler.shared.deleteMovie(movie)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.tableView.reloadData()
}
}
You must not use the captured index path if cells can be deleted, inserted or moved.
In the custom cell declare the closure
var deleteButtonActionBlock : ((UITableViewCell) -> Void)?
and call it
deleteButtonActionBlock?(self)
In cellForRow get the actual index path for the cell
cell.deleteButtonActionBlock = { aCell in
let actualIndexPath = tableView.indexPath(for: aCell)!
FavouritesHandler.shared.deleteMovie(movie)
tableView.deleteRows(at: [actualIndexPath], with: .automatic)
}
No (ugly) delay and no (pointless) reloading needed.
Please note also that I removed the self in the delete line because the tableView instance is available as method parameter.
Related
I'm using the UICollectionViewDiffableDataSource to populate my UICollectionView. After receiving a list of items via REST API, I create a new snapshot and apply it like this:
DispatchQueue.main.async {
var snapshot = NSDiffableDataSourceSnapshot<RegionSection, DiffableModel>()
snapshot.appendSections(RegionSection.allCases)
snapshot.appendItems(self.spotlights, toSection: .Spotlights)
snapshot.appendItems(self.vendors, toSection: .Vendors)
self.dataSource?.apply(snapshot, animatingDifferences: animated)
}
When setting up my cells in the cellProvider, I asynchronously load images from a URL. I noticed, that the first cell would frantically flick through all the images that are loaded and end up displaying a different image than it was supposed to. (For example the image intended to be displayed by the last cell).
I decided to investigate and figured out that the cellProvider closure is called twice as many times as expected. Also the collectionView.dequeueReusableCell function behaves weirdly for the first half of the calls as it returns the same cell each time even though there are no cells in the collectionView that could be dequeued.
My cellProvider closure:
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, entry) -> UICollectionViewCell? in
if let spotlight = entry as? Spotlight{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "spotlightCell", for: indexPath) as! SpotlightCell
cell.nameLabel.text = spotlight.title
cell.subtitleLabel.text = spotlight.subtitle
cell.categoryLabel.text = spotlight.type.getDescription().uppercased()
cell.imageView.loadImage(fromUrl: spotlight.titlePictureUrl)
return cell
}else if let vendor = entry as? Vendor{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "vendorCell", for: indexPath) as! VendorCell
cell.nameLabel.text = vendor.title
cell.assortmentLabel.text = vendor.assortmentDescription
cell.imageView.loadImage(fromUrl: vendor.titlePictureUrl ?? vendor.pictureUrls?.first ?? "")
if let distance = vendor.distance{
cell.distanceLabel.text = (distance/1000) < 1 ? (distance.getReadableString(withDecimalSeparator: ",", andDecimalCount: 0) + "m entfernt") : ((distance/1000).getReadableString(withDecimalSeparator: ",", andDecimalCount: 0) + "km entfernt")
}
return cell
}
return nil
}
Here is an example:
I create a snapshot containing 4 vendor entries (For simplicity I didn't add anything in the other section for this example)
The cellProvider is called 4 times (for each indexPath and entry) and the cell that is dequeued is the same one each time.
The cellProvider is called another 4 times (again, for each indexPath and entry) and this time the cells are different each time.
For each time the cellProvider is invoked I call loadImage, which tries to find an image for the URL in my image cache and if it cannot found one loads it asynchronously.
Since all calls happen almost simultaneously every image is loaded twice and the first cell displays one image after another until the last of the 4 URLSessions it initiated returns.
I can't imagine it is expected behaviour for the dataSource to call it's cellProvider closure that often and I simply can't figure out why this happens or find anything in the documentation on this.
I hope someone can explain to me why this happens and in case this is expected behaviour how to properly set up cells with asynchronous image loading using a DiffableDataSource.
EDIT:
The solution that worked for me was to use absolute instead of estimated sizes for my cells, as suggested by #Norb Braun!
Setting the estimated size to none fixed this issue for me. This solution may not work for you when you are required to use self sizing cells but if your cells keep the same size regardless the content you could give it a try.
ReloadItems does not seem to allow for animation.
I tried using collectionView:( willDisplay) but the reloadItems() actually overtakes the animation (i.e. cutting it off and reloading the cell)
I tried performBatchUpdates() but I get outOfRange error in this block:
collectionView.performBatchUpdates({
// reload one or more items
collectionView.reloadItems(at: [tappedArray[0], tappedArray[1]])
}) { (_) in
// do animations here
if let cell1 = collectionView.cellForItem(at: self.tappedArray[1]) {
self.animateCell(cell1)
}
}
If a single item in a collectionView is reloaded, how can I add animation(s) to the re-display of that item?
Issue resolution: Animation done in a closure is NOT on the main thread. Since all animation should be on the main thread, this was causing my UI to glitch, or "not allow for animation".
I'm using Firebase to create a networking app for a client.
For every user, I add an observer to its Firebase property timestamp, that means that when the user uploads a status update, the Firebase observer gets triggered so that the users cell in the feed move to the top (every user only has one cell). Here's the Firebase code:
Database.database().reference().child(forUser.userID).child("timestamp").observe(.value) { (snapshot) in
if let timestamp = snapshot.value as? Int {
if let cell = self.feedCollectionView.cellForItem(at: indexPath) as? FeedCell {
self.feedCollectionView.moveItem(at: indexPath, to: IndexPath(row: 0, section: 0))
}
}
}
It is though very well possible that I have 100-200 users and maybe the last of them updates his status and has to be moved to the top, even though his collection view cell has never even been loaded.
When I run this, it prints for every visible cell that it is movable and for every invisible cell that it can't access the unloaded cell.
How can I access a cell that has never been loaded and move it to the top?
Instead of moving a cell that may or may not be loaded, you should update the collectionview's datasource by sorting it on timestamp. Then you can call self.feedCollectionView.reloadData() and it'll place it on the top automatically.
I am currently using a folding cell library and when i close the cell, the labels(dates) and images(workout type) should reload.
tableView.beginUpdates()
UIView.performWithoutAnimation {
tableView.reloadRows(at: indexSet, with: .none)
}
tableView.endUpdates()
I have tried putting my reloadRows in a dispatchQueue.main.async call and everything else that is similar to mine question on stack overflow.
did you try self.tableView.reloadData()?
This type of problem normally occurs when you have not reloaded your tableViews , i.e your Data Source method cellForRowAtIndexPath is not populate tableview elements .
so , try with reload tableView like below
self.tableView.reloadData()
I'm inserting new items as the user scrolls down the collection view... I'm having an issue where if they scroll too fast the collection view stops scrolling when the new items are inserted.
My original issue was that I was just calling self.collectionView.reloadData() instead of performing batch updates but it's currently doing the same exact thing...
This is my code
let postIndex = self.posts.count
let newPostIndex = newPosts.count
let indexArray = (postIndex...postIndex+newPostIndex-1).map{IndexPath(item: $0, section: 0)}
self.posts.append(contentsOf: newPosts)
self.collectionView.performBatchUpdates({
self.collectionView.insertItems(at: indexArray)
}, completion: nil)
I already have posts so I create an array of the indexes that I need to update and insert them in the performBatchUpdates but it looks really choppy and lacks a smooth feel.
I tried CATransaction as suggested here https://stackoverflow.com/a/32691888/6513002 and it literally scrolled all the way down completely bypassing everything new and going to the end..
Please assist