UICollectionViewCell not updating image if visible - ios

I have a CollectionView which shows thumbnails after loading. At the beginning, the pictures are not stored on the device so they get fetched from the server. In a callback method after fetching these images from the server they cells don't get updated, i need to scroll them out of the screen, then they get updated. I had a similar mechanism in another app, there everything worked fine and I can't find the difference. Below are some parts of the code for better understanding what I do.
This is the code where i set the image after checking if the thumbnail is already fetched from the server. If so, set it directly, if not, retrieve the picture using Alamofire and update it after finish loading.
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath) as! PictureCollectionViewCell
/* Set thumbnail to the cell */
if let thumbnail = pictures[indexPath.row].thumbnail {
cell.imageView.image = thumbnail
} else {
pictures[indexPath.row].retrievePicture(thumbnail: true, completion: { picture in
cell.imageView.image = picture
})
}
return cell
}
As said before, it's not updating until I scroll the cell out of the visible screen area. cell or collectionView reloads didn't fix the problem. Any ideas?
Thanks in advance.

because the block execute in background thread. you have to update UI in main thread. use this code below
pictures[indexPath.row].retrievePicture(thumbnail: true, completion: { picture in
DispatchQueue.main.async {
cell.imageView.image = picture
}
})

You might find it better to simply reload the items at the indexPath instead of reloading the entire table using:
collectionView.reloadItemsAtIndexPaths(yourIndexPath)

Fetching is probably happening in the background thread so your callback function is called from it too. UI cannot be updated from the the background thread.
So in your fetching callback try to use something like this:
DispatchQueue.main.async {
collectionView.reloadData()
}
Or
DispatchQueue.main.async {
collectionView.reloadItems(indexPaths)
}

Ok guys, it was the dumbest error you can imagine. Updating the UI from an asynchronous callback method just works fine. I just forgot to call the callback method from the async task. Problem solved, thank you for your help.

Related

UICollectionView reloadData not working

Its getting called in viewDidLoad, after fetching the data used.
After some print debugging it looks like it calls all the appropriate delegeate methods, if no data is changed. If there has been some data changed, cellForItemAt does not get called.
Reloading the whole section works fine. But gives me an unwanted animation. Tried disabling UIView animation before, and enabling after reloading section, but still gives me a little animation.
collectionView.reloadSections(IndexSet(integer: 0))
Here is my current situation, when using reloadData()
The UICollectionViewController is a part of a TabBarController.
I'm using custom UICollectionViewCells. The data is loaded from CoreData.
First time opening the tab, its works fine.
After updating the favorites item in another tab, and returning to this collectionView, its not updated. But if i select another tab, and go back to this one, its updated.
var favorites = [Item]()
override func viewWillAppear(_ animated: Bool) {
collectionView!.register(UINib.init(nibName: reuseIdentifier, bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
if let flowLayout = collectionView!.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 1,height: 1)
}
loadFavorites()
}
func loadFavorites() {
do {
print("Load Favorites")
let fetch: NSFetchRequest = Item.fetchRequest()
let predicate = NSPredicate(format: "favorite == %#", NSNumber(value: true))
fetch.predicate = predicate
favorites = try managedObjectContext.fetch(fetch)
if favorites.count > 0 {
print("Favorites count: \(favorites.count)")
notification?.removeFromSuperview()
} else {
showEmptyFavoritesNotification()
}
print("Reload CollectionView")
collectionView!.reloadData(
} catch {
print("Fetching Sections from Core Data failed")
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("Get number of section, \(favorites.count)")
if favorites.count > 0 {
return favorites.count
} else {
return 0
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("Get cell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SubMenuCell
let menuItem = favorites[indexPath.row]
cell.title = menuItem.title
cell.subtitle = menuItem.subtitle
return cell
}
Console Print/Log
Starting the app, going to the CollectionView tab where there are no favorites:
Load Favorites
Get number of section, 0
Get number of section, 0
Reload CollectionView
Get number of section, 0
Switching tab, and adding and setting an Item object's favorite to true, then heading back to the CollectionView tab:
Load Favorites
Favorites count: 1
Reload CollectionView
Get number of section, 1
The datamodel has 1 item, reloading CollectonView, cellForRowAtIndex not called?
Selecting another tab, random which, then heading back to the CollectionView tab, without changing any data.
Load Favorites
Favorites count: 1
Reload CollectionView
Get number of section, 1
Get cell
Now my Item shows up in the list. You can see from the Get Cell that cellForItemAt is called aswell. Nothing has changed between these last two loggings, just clicked back/fourth on the tabs one time.
Forgot to mention, but this code IS working fine in the simulator.
On a read device its just not giving me any error, just not showing the cell (or calling the cellForItemAt)
After some more debugging i got an error when the items got reduced (instead of increasing like i've tried above).
UICollectionView received layout attributes for a cell with an index path that does not exist
This led med to iOS 10 bug: UICollectionView received layout attributes for a cell with an index path that does not exist
collectionView!.reloadData()
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.layoutSubviews()
This solved my problem.
I am using autoresizing cells, but i guess this does not explain why cellForItemAt did not get called.
in my case, my collectionview was in a stackview, i didnt make any constraints, when i added the necessary constraints it worked.
Just check collectionview's constraints.
Ok, maybe I can help someone =)
I do this:
Load data from API
Then after load data call DispatchQueue.main.async like this
DispatchQueue.main.async {
self.pointNews = pointNews
}
In self.pointNews didSet I try to do collectionView.reloadData(), but it work only, if I call DispatchQueue.main.async in didSet
DispatchQueue.main.async {
self.collectionView.reloadData()
}
var paths: [IndexPath] = []
if !data.isEmpty {
for i in 0...salesArray.count - 1 {
paths.append(IndexPath(item: i, section: 0))
}
collectionView.reloadData()
collectionView.reloadItems(at: paths)
}
This is the code helped me, for solving this kind of issue.
Post your code.
One possibility is that you are using `URLSession and trying to tell your collection view to update from the it's delegate method/completion closure, not realizing that that code is run on a background thread and you can't do UI calls from background threads.
I faced the same problem with self resizing cells and above solution works as well but collection view scroll position resets and goes to top. Below solution helps to retain your scroll position.
collectionView.reloadData()
let context = collectionView.collectionViewLayout.invalidationContext(forBoundsChange: collectionView.bounds)
context.contentOffsetAdjustment = CGPoint.zero
collectionView.collectionViewLayout.invalidateLayout(with: context)
collectionView.layoutSubviews()
i was facing also this kind of issue so finally i am able to solved using this line of code
[self.collectionView reloadData];
[self.collectionView reloadItemsAtIndexPaths:#[[NSIndexPath indexPathWithIndex:0]]];
I had the same issue. I was updating the height of the UICollectionView based on the content and reloadData() stops working when you set the height of the collection view to zero. After setting a minimum height. reloadData() was working as expected.

UICollectionView load image cannot get correct cell swift 3

My UICollectionView is fetching images in cellForItemAt. After I upgrade to swift 3, some images are not showing when I scroll very fast. Here is my code in cellForItemAt:
if (imageNotInCache) {
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
if let thumb = imageFromPathAndCacheIt(path) {
DispatchQueue.main.async {
let updateCell = collectionView.cellForItem(at: indexPath) as! MyCustomCell? {
updateCell.imageView.image = thumb
}
}
}
The problem is, sometimes I get updateCell as nil even it is on the screen (probably because scroll to fast).
I know we can add reloadData after adding the image, just like: Swift 3: Caching images in a collectionView, but I don't want to call reload so many times.
Thank you for interested in this question. Any ideas would be very appreciated!
There's definitely a compromise between accessing and manually updating the cell view content, and calling reloadData on the whole collection view that you could try.
You can use the func reloadItems(at: [IndexPath]) to ask the UICollectionView to reload a single cell if it's on screen.
Presumably, imageNotInCache means you're storing image in an in-memory cache, so as long as image is also accessible by the func collectionView(UICollectionView, cellForItemAt: IndexPath), the following should "just work":
if (imageNotInCache) {
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
if let thumb = imageFromPathAndCacheIt(path) {
DispatchQueue.main.async {
self.collectionView.reloadItems(at: [indexPath])
}
}
}
}

Lazy image downloading on UITableViewCell using defer

Is it safe and does it make sense to defer an asynchronous image download for a cell? The idea behind this is that I want the callback function from URLSession.shared.image(...) to be executed after creating the cell and only once calling cellForRow(at: indexPath) is valid, since I think that without deferring this method at this point should return nil.
URLSession.shared.image is a private extension that runs a data task and gives a escaping callback only if the url provided in the argument is valid and contains an image.
setImage(image:animated) is a private extension that allows you to set an image in a UIImageView using a simple animation.
If defer is not the way to go, please indicate an alternative.
Any feedback is appreciate, thanks!
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = baseCell as! MyCell
let datum = data[indexPath.row]
cell.imageView.setImage(placeholderImage, for: .normal)
defer {
URLSession.shared.image(with: datum.previewURL) { image, isCached in
if let cell = tableView.cellForRow(at: indexPath) as? MyCell {
cell.imageView.setImage(image, animated: !isCached)
}
}
}
return cell
}
NSHipster have a good article on how / when to use defer, here.
I wouldn't use defer in such a way. The intention for defer is to clean up / deallocate memory in one block of code, rather than scattering it across many exit points.
Think about having multiple guard statements throughout a function and having to deallocate memory in every one of them.
You shouldn't use this to simply add additional code after the fact.
As mentioned by #jagveer there are many third party libraries that do this already, such as SDWebImage cache. AFNetworking and AlamoFire also have the same built in. No need to re-invent the wheel when its already been done.
In this case, the defer isn't being effective. defer sets up a block to be called when the scope exits, which you are doing immediately.
I think what you want to schedule the block to run in a different thread using Dispatch. You need to get back onto the main thread to update the UI.
As this can happen later, you need to make sure the cell is still being used for the same entry and has not been reused as the user has scrolled further. Fetching the cell again isn't a good idea if it has been reused as you'd end up triggering the initial call again. I usually add some identifier to a custom UITableViewCell class to check against.
Also, you're not creating the cell, just fetching it from some other variable. This is likely to be a problem, if there is more than one cell.
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "base") as! MyCell
cell.row = indexPath.row
let datum = data[indexPath.row]
cell.imageView.setImage(placeholderImage, for: .normal)
DispatchQueue.global().async {
// Runs in a background thread
URLSession.shared.image(with: datum.previewURL) { image, isCached in
DispatchQueue.main.async {
// Runs in the main thread; safe for updating the UI
// but check this cell is still being used for the same index path first!
if cell.row == indexPath.row {
cell.imageView.setImage(image, animated: !isCached)
}
}
}
}
return cell
}

Swift - How do I stop a table from reloading an image if it's the same image as before?

I have an application that runs a bunch of json API requests to a server that then loads info and an image alongside that info. The problem is that when I move down across a table and then back up again, it reloads the images with the animation I set (alpha = 0 to 1) and I don't want that since it looks bad. I tried telling the server if the newImage == cell.imageview.image but that of course didn't work since it's not the same image in memory. How can I fix this?
The only way I can think of is to subclass or extend the UIImage class and add a string that is the link to the image, and then when I'm setting the image, I also check if the links are the same. Any thoughts?
You can keep an dictionary/set of indexPaths that have completed the animation. So you check for indexPath in your dictionary just before you add the animation and on animation completion add the indexPath. your code will look something like this:
//in your viewController class
var animateIndexPaths = Set<NSIndexPath>()
//tableview delegate
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if !animateIndexPaths.contains(indexPath) {
UIView.animateWithDuration(0.5, animations: {
//do your animation
}, completion: { (stop:Bool) in
self.animateIndexPaths.insert(indexPath)
})
}
}

Using PINRemoteImage to populate CollectionViewCell Images

I make a network call in ViewDidLoad to get objects (first 25), then I make another call in willDisplayCell to get the rest of the objects. I'm Using PINReMoteImage in the code below. It works, but the problem is that as you scroll through the collection view, a cell will have one picture then another picture will appear over it. How can I improve this UI? I thought updateWithProgress was supposed to deal with this by using a blur until the image is loaded but it doesn't seem to be working?
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PinCollectionViewCell
if let pinImageURL = self.pins[indexPath.row].largestImage().url {
cell.pinImage?.pin_updateWithProgress = true
cell.pinImage?.pin_setImageFromURL(pinImageURL, completion: ({ (result : PINRemoteImageManagerResult) -> Void in
if let image = result.image {
self.imageArray.append(image)
}
}))
}
The problem is that I'm reusing cells, but I wasn't resetting the image. So I simply added cell.pinImage?.image = nil to the above. Once I found out that was the problem I added an UIActivityViewIndicatorViewto the cell and stop it when the image comes in.

Resources