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.
Related
I know that this question was asked a couple of times, but other solutions didn't work for me.
I have this model:
var notifications = [Notification]()
Notification:
let user: User (name, profileimage, etc)
let items: [Movies] (movie has image, name, etc)
So i display my notifications inside a tableview, each cell has profile info at the top and collectionview with items bellow.
Inside my tableviewcell, i have to reloadData of my collectionview to display correct movies. I know that probably reloadData method causes this lagging, but are there any solutions to avoid it?
TableViewcell:
var notification: Notification! {
didSet {
collectionView.reloadData();
}
}
I also tried this thing inside tablecell and call this method in willDisplayTableViewcell, but it doesn't help at all:
func setCollectionViewDataSourceDelegate(forRow row: Int) {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.reloadData()
}
Images are loading using kingfisher, so it's fine in other places in my project.
userProfileImage.kf.setImage(
with: URL(string: profileImage),
options: [
.processor(DownsamplingImageProcessor(size: CGSize(width: 175, height: 175))),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(0.2)),
]
)
I would comment out the code you have shown above as it will only get in the way and give false readings. My gut feeling is somehow you have managed to directly wire your notifications to your tableview / collection view and they are running on a background thread. What I would do to test this theory is wrap code anywhere where the UI is going to be updated with a perform on main thread like this
DispatchQueue.main.async { [unowned self] in
//Your UI update code within cellforRow / cellForItem (both table and collection view) Here
}
Try doing this one by one until you find the culprit and for a better solution try to refactor so you don't have to force this Main thread directive.
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.
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.
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
I have a UICollectionView that is populated with all photos from the devices photo library. After a cell (photo) is tapped, it segues to a view controller that allows for editing. On this view, there is an "Add Photo" button to return the user back to the UICollectionView (to select another photo). I need for the scroll position to "focus" the previous tapped cell in the center of the view without any animations or jumping.
I have tried saving the tapped indexPath as a variable, then on viewDidAppear, scroll to that indexPath with scrollToItemAtIndexPath. The problem is I can't figure out how to update a variable (to save indexPath) on cell tap. I tried this in didSelectItemAtIndexPath, but the value never actually saves.
var cellTappedIndexPath = Int()
Inside didSelectItemAtIndexPath:
cellTappedIndexPath = indexPath.row
The value for cellTappedIndexPath never saves.
Just for testing out scrollToItemAtIndexPath, I have added the following to viewDidAppear:
customViewCollectionView.scrollToItemAtIndexPath(NSIndexPath(forItem: 25, inSection: 0), atScrollPosition: UICollectionViewScrollPosition.CenteredVertically, animated: false)
// 25 is just a number I have set for testing. Ultimately, I would like this to be the saved indexPath of the last tapped cell.
This causes the collectionView to "jump" to cell 25 once it's fully loaded. If I set animated to true, it loads at the top, then scrolls down to cell 25. Not my desired result.
I just want to be able to do 2 things here.
1 - Save the cell tapped as a variable.
2 - use scrollToItemAtIndexPath (with the variable in #1) so the view just loads up instantly with the last cell tapped right into the middle, No animations or anything.
Let me know if further clarification is needed. THANKS!
You could save the selected indexPathreceived when the collectionview cell is tapped and use it when required.
var selectedIndexPath: NSIndexPath?
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
selectedIndexPath = indexPath
}
func scrollToCell(){
if let index = selectedIndexPath{
customViewCollectionView.scrollToItemAtIndexPath(index, atScrollPosition: .CenteredVertically, animated: false)
}else{
print("A cell hasnt been selected yet")
}
}