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])
}
}
}
}
Related
Background of problem: I am using Kingfisher to download and set images for all of my collection views and table views in my app. This works well, but I have one specific use case that I'm pretty sure Kingfisher doesn't have built in.
What I am trying to do: Download multiple images for a single collection view cell, and then update the cell with the images.
Why I am not using .kf.setImage(...): The collection view cell UI is all drawn from draw(_ rect: CGRect)
What I am doing now: In my UICollectionViewCell I have an image array
var posters: [UIImage] = [] {
didSet {
setNeedsDisplay()
}
}
and I draw them into the cell
context.draw(image, in: CGRect(x: 0, y: -posterWindowFrame.minY, width: actualPosterWidth, height: actualPosterHeight))
In my view controller I load images in willDisplayCell:forItemAtIndexPath: like so
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let imageProviders = viewModel.imageProviders(at: indexPath)
var images = [UIImage]()
let group = DispatchGroup()
let cachedIndexPath = indexPath
imageProviders.forEach { provider in
guard let provider = provider else { return }
group.enter()
_ = KingfisherManager.shared.retrieveImage(with: .provider(provider), options: [.targetCache(posterCache)]) { result in
DispatchQueue.main.async {
defer {
group.leave()
}
switch result {
case .success(let imageResult):
images.append(imageResult.image)
case .failure:
print("No image available")
}
}
}
}
group.notify(queue: .main) {
guard
let cell = collectionView.cellForItem(at: cachedIndexPath) as? ListCollectionViewCell
else { return }
cell.posters = images
}
}
This works, but I can see the images changing (which could probably be fixed by pre-fetching) but I also notice a lot of lag when scrolling that I don't see when I comment this function out.
Besides fetching all of the images in ViewDidLoad (or anywhere else, because of API rate limiting), how can I load these images in a way that doesn't cause lag and won't have problems with cell reuse?
EDIT - 8/13/19
I used UICollectionViewDataSourcePrefetching to fetch the images and cache them locally, and then set the images in cellForRow and it seems to work well for now.
I’m working on a photo gallery app that consists essentially of a navigation controller on to which is loaded instances of a collection view. Each of the collection view cells contains a UIImageView, and a label for the name of the image. The cells load the images in asynchronously.
This all works fine the first time each of the collection views are loaded, but if you step back to a previous collection view and forward again the labels disappear.
I think it must have something to do with loading the images asynchronously, as if I remove the image load the labels are fine, have the correct text and don’t disappear.
I’m not sure where I’m going wrong with the image loading...
The various components are:
Images are loaded using this extension method:
extension UIImageView {
public func imageFromServerURL(url: URL) {
if ImageData.realPhotoCache.keys.contains(url.absoluteString){
self.image = ImageData.realPhotoCache[url.absoluteString];
}else{
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error)
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
ImageData.realPhotoCache[url.absoluteString] = image;
self.image = image
})
}).resume()
}
}}
And the cells for the collection view are contucted like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell: UICollectionViewCell;
cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellReuseIdentifier, for: indexPath);
let imageProvider = self.imageProviderForIndexPath(indexPath: indexPath);
(cell as! tsPhotoCollectionViewCell).photoView.imageFromServerURL(url: imageProvider.GetThumbImageUrl());
(cell as! tsPhotoCollectionViewCell).titleLabel.text = imageProvider.GetImageTitle()!;
return cell
}
with the collection view being reset when it is returned to like so:
func reset(){
photoProviders = [ImageDataProvider]();
self.collectionView?.reloadSections(IndexSet(integer: 0))
startLoad()
}
override func viewWillAppear(_ animated: Bool) {
if(hasNavigatedAway){
hasNavigatedAway = false;
self.reset();
}
}
The first load of a collection will generally be fine (cells in green, labels in red):
https://imgur.com/g3p03yF
but on naving away and coming back to a collection view, the labels are gone:
https://imgur.com/If2FQZS
The labels also seem to come and go with scrolling sometimes. I've tried everything I can think of, but haven't had any luck..
Judging by your screenshots, it seems like UILabels are getting their heights compressed to 0 by expanded UIImageViews. Try increasing UILabel's vertical content compression resistance priority, say, to 1000, so that it becomes non-compressible (and, in turn, makes auto layout engine compress either UIImageView or spacings - depends on your constraints).
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.
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.
First, I create a collection view controller with a storyboard, and subclass a cell (called RouteCardCell).
The cell lazy loads an image from Web. To accomplish this, I create a thread to load the image. After the image loads, I call the method reloadItemsAtIndexPaths: to display the image.
Loading the image works correctly, but there's a problem displaying the image. My cells display the new image only after scrolling them off-screen and back on.
Why don't my images display properly after reloading the item?
Here's the relevant code:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as RouteCardCell
let road = currentRoads[indexPath.item]
cell.setText(road.title)
var imageData = self.imageCache.objectForKey(NSString(format: "%d", indexPath.item)) as? NSData
if let imageData_ = imageData{
cell.setImage(UIImage(data: imageData_))
}
else{
cell.setImage(nil)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var Data = self.getImageFromModel(road, index:indexPath.item)
if let Data_ = Data{
self.imageCache.setObject(Data_, forKey: NSString(format: "%d", indexPath.item))
NSLog("Download Image for %d", indexPath.item)
}
else{
println("nil Image")
}
})
self.reloadCollectionViewDataAtIndexPath(indexPath)
}
return cell
}
func reloadCollectionViewDataAtIndexPath(indexPath:NSIndexPath){
var indexArray = NSArray(object: indexPath)
self.collectionView!.reloadItemsAtIndexPaths(indexArray)
}
func getImageFromModel(road:Road, index:Int)->NSData?{
var images = self.PickTheData!.pickRoadImage(road.roadId)
var image: Road_Image? = images.firstObject as? Road_Image
if let img = image{
return img.image
}
else{
return nil
}
}
You're calling reloadCollectionViewDataAtIndexPath(indexPath) before the image is done downloading. Instead of calling it outside of your dispatch_async block, add another block to go back on the main queue once it's done.
For example:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
// download the image…
// got the image, now update the UI
dispatch_async(dispatch_get_main_queue()) {
self.reloadCollectionViewDataAtIndexPath(indexPath)
}
})
This is a pretty tough problem in iOS development. There are other cases you haven't handled, like what happens if the user is scrolling really quickly and you end up with a bunch of downloads that the user doesn't even need to see. You may want to try using a library like SDWebImage instead, which has many improvements over your current implementation.