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).
Related
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])
}
}
}
}
I am using a collection view with images. Lets say, it have 30 images.
Whenever I scroll the view, the image reloads. It is because, whenever I scroll, this function is called.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
Now my issue is: I fetch few images from facebook and few from an array. So whenever I scroll, the images from fb alone reloads. How can I stop reloading those images.
Here's my code for displaying image(from fb and local):
let task = NSURLSession.sharedSession().dataTaskWithURL(searchURL) { (responseData, responseUrl, error) -> Void in
if let data = responseData{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.image!.image = UIImage(data: data)
if(cell.image!.image == nil){
let strid : String = friendInfo.Friendfb_id;
let facebookProfileUrl = NSURL(string: "https://graph.facebook.com/\(strid)/picture?type=large")
cell.image!.url = facebookProfileUrl;
}else{
cell.image!.url = searchURL;
}
})
}
}
task.resume()
I have also set prepareForReuse in the cell:
override func prepareForReuse() {
self.imageView.image = nil
}
You must implement a cache of some sort to cache the images, so when the same cell is displayed for the second time the image should be loaded from cache not from the server. There are multiple solutions for implementing a cache like this, my personal favorite is using AlamofireImage to load the images. It does the loading and caching for you.
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.
I'm following a site to help learn swift and I'm getting confused about this part right here. Basically we added the if cell.imageview.image == nil statement so hat when the collection view loads and you scroll the image doesn't reload the filters. What I don't understand is if you scroll down a cell is reused for the bottom row, now why if I scroll back up doesn't it have to reload the filter? is that data saved somewhere so when I scroll up the properties don't have to repopulate? and If thats the case why would I have to use that if statement at all?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as! FilterCell
if cell.imageView.image == nil {
cell.imageView.image = placeholder
let filterQueue: dispatch_queue_t = dispatch_queue_create("filter queue", nil)
dispatch_async(filterQueue, { () -> Void in
let filterImage = self.filteredImageFromImage(self.thisFeeditem.thumbNail, filter: self.filters[indexPath.row])
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.imageView.image = filterImage
})
})
}
return cell
}
When a cell is reused, a cell object that was already allocated is just used again. Any properties or data that was set to it will remain.
When you scroll back up, the cell has already had it's image set, and so it won't reload the new filtered image.
Cells can be reused whether you scroll up or down. You should assume the cell returned is a cached version for a different item. Therefore it may already be bound with another cell's data and you'd want to always rebind the cell with the proper items' data.
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.