UICollectionViewCell Reuse with Asynchronous Image Loading wrong image - ios

I am creating a UICollectionView that initially downloads images from an AWSS3 bucket, then caches the images for later access. The problem is that since the download takes some time, when the user scrolls the UICollectionView more downloads are queued up. The first batch of downloads finishes and loads into the newly visible cells, then the second batch of downloads finishes and replaces the same cells.
This results in images appearing in cells they should not. I have attempted the solutions from similar posts but have not found one works.
Here is a simplified version of what I have tried
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = self.collectionView.dequeueReusableCellWithReuseIdentifier("imageCell", forIndexPath: indexPath) as! GroupImageCell
cell.imageView.image = nil
getPhotoForCell(cell, indexPath)
}
func getPhotoForCell(cell: GroupImageCell, idx: NSIndexPath)
{
#use AWSS3downloadrequest
#once results are in
if (task.result != nil) {
#get body of result, then url, then data
let data = NSData(data: task.result.body as! NSURL)
let image = UIImage(data: data)
if (collectionView.cellForItemAtIndexPath(idx) != nil)
{
dispatch_async(dispatch_get_main_queue(), {
cell.imageView.image = image
})
}
}
}

You can fix this issue by storing the images in your model object that is used for each cell. Meaning, instead of
dispatch_async(dispatch_get_main_queue(), {
cell.imageView.image = image
})
Do something like this
dispatch_async(dispatch_get_main_queue(), {
items[indexPath.row].image = image
})
This way you will guarantee to render the correct image when you call from your cellForItemAtIndexPath method.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = self.collectionView.dequeueReusableCellWithReuseIdentifier("imageCell", forIndexPath: indexPath) as! GroupImageCell
cell.imageView.image = items[indexPath.row].image
}

Related

Why collectionView freezes on scroll?

I'm trying to create a simple app with photo filters, like Instagram. So, I capture my image then I need to implement filter to this image. On the bottom I have a UICollectionView(horizontal scroll) where I can see what filters I have and how they will look on my image.
But when I scroll my UICollectionView - it freezes and my filters applying on every new cell(because of reusing process). But how Instagram does it that when I scroll the filters they does not freeze?
I've tried this:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: imageFilterCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! imageFilterCell
let queue = NSOperationQueue()
let op1 = NSBlockOperation { () -> Void in
let img = UIImage(data: UIImageJPEGRepresentation(self.image!, 0.1)!)
let img1 = self.applyFilterTo(img!, filter: self.filtersImages[indexPath.row])
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
cell.imageView.image = img1
})
}
queue.addOperation(op1);
return cell
}
but it still freezes on scroll and I see how every time my filters are applying to my cells. Can I do it just one time and then on scroll to do nothing, just showing how the photo will looks after implementing the filter?
You could do one of the two things:
Do not use the reuse feature of the collection view. Simply create
an array of cells and pass those cells to cellForRowAtIndexPath. This should work fine if you
don't have a large number of filters. Something like:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell: imageFilterCell? = nil
if (cellsArray.count <= indexPath.row) {
//This assumes imageFilterCell is created in code
//If you use storyboards or xibs load it accordingly
cell = imageFilterCell()
cellsArray.append(cell)
} else {
cell = cellsArray[indexPath.row]
}
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let img = UIImage(data: UIImageJPEGRepresentation(self.image!, 0.1)!)
let img1 = self.applyFilterTo(img!, filter: self.filtersImages[indexPath.row])
dispatch_async(dispatch_get_main_queue()) {
cell!.imageView.image = img1
}
}
return cell
}
Implement a cancel mechanism for the async operation, because if the cell is reused while the background thread is still applying the filter the final cell will end up having multiple filters. For this you could use the cancel method from the NSOperation class. For this you could create a property on your custom cell called operation something like:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: imageFilterCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! imageFilterCell
cell.operation.cancel()
//make the queue an instance variable
//let queue = NSOperationQueue()
let op1 = NSBlockOperation { () -> Void in
let img = UIImage(data: UIImageJPEGRepresentation(self.image!, 0.1)!)
let img1 = self.applyFilterTo(img!, filter: self.filtersImages[indexPath.row])
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
cell.imageView.image = img1
})
}
queue.addOperation(op1);
cell.operation = op1
return cell
}
You are processing in the main thread, this is why it freezes.
Use a Concurrent Dispatch Queue for a more fluid experience.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: imageFilterCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! imageFilterCell
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let img = UIImage(data: UIImageJPEGRepresentation(self.image!, 0.1)!)
let img1 = self.applyFilterTo(img!, filter: self.filtersImages[indexPath.row])
dispatch_async(dispatch_get_main_queue()) {
cell.imageView.image = img1
}
}
return cell
}
you can cache data (Images) in memory
its problem have a lovely relation with read data(images) :D

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.

UICollectionView CustomCell ReUse

I am fetching user's Photo Asset and trying to create a custom UICollectionViewCell in my UICollectionView where the first index is a Camera Image and other cells are the Camera Images. But when I scroll, it's like the image at the index is recreated and I notice some images in my cell get the view I added at the index[0]
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//Deque an GridViewCell
let cell: GridViewCell = (collectionView.dequeueReusableCellWithReuseIdentifier(CellReuseIdentifier, forIndexPath: indexPath) as? GridViewCell)!
if indexPath.item != 0 {
let asset = assetsFetchResults[indexPath.item - 1] as! PHAsset
cell.representedAssetIdentifier = asset.localIdentifier
imageManager.requestImageForAsset(asset, targetSize: AssetGridThumbnailSize, contentMode: PHImageContentMode.AspectFill, options: nil) { (result: UIImage?, info: [NSObject : AnyObject]?) -> Void in
//print(result)
if cell.representedAssetIdentifier.isEqualToString(asset.localIdentifier) {
if let imageResult = result {
cell.imageView.image = imageResult
}
}
}
} else {
let cameraButton = UIButton(frame: CGRectMake(0,0,80,80))
cameraButton.setTitle("Camera", forState: .Normal)
cameraButton.frame = cell.imageView.bounds
cell.imageView.addSubview(cameraButton)
}
return cell
}
Below is an image for illustration
Reusable cells are reused by the collection view when they are scrolled outside the view to prevent a lot of memory use. When a certain cell gets scrolled outside of the view the collection view will mark them as reusable. Reusable cells will be reused by the collection view instead of creating new ones. This will significantly reduce memory use. Why would you keep 1000 cells in memory if only 20 of them fit on the screen at the same time.
because cells are reused, they will still contain the content of previous cell, this means that the cell still has the cameraButton as a subview. Same story with the images, you need to manually remove them at from the cell and make sure al old content gets replaced or removed.
When you perform methods that will download and set the image to the cell after a while, the cell could still contain the image of the previous cell. You should clear the image at the beginning of the cellForRowAtIndexPath method to remove the old image/subviews.
See example below:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! GridViewCell
if indexPath.row == 0 {
// Show the title becouse it's the first cell.
cell.titleLabel.hidden = false
// Title needs to be set in storyboard
// Default image for camera roll needs to be set in storyboard
} else {
// All the other cells.
cell.titleLabel.hidden = true
// Clear old content
cell.imageView.image = nil
// I don't exactly know what all this code does behind the screen, but I assume it's a method that downloads the image and add it to the imageView when it's done.
let asset = assetsFetchResults[indexPath.item - 1] as! PHAsset
cell.representedAssetIdentifier = asset.localIdentifier
imageManager.requestImageForAsset(asset, targetSize: AssetGridThumbnailSize, contentMode: PHImageContentMode.AspectFill, options: nil) { (result: UIImage?, info: [NSObject : AnyObject]?) -> Void in
//print(result)
if cell.representedAssetIdentifier.isEqualToString(asset.localIdentifier) {
if let imageResult = result {
cell.imageView.image = imageResult
}
}
}
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// Cell that needs to open the camera roll when tapped)
if indexPath.row == 0 {
// Do something that opens the camera roll
}
}
I didn't test this code, but it should be something similar to this.

iOS: Keeping User Interface responsive while images are being loaded in a separate thread

Some context: I have a UICollectionView which will display around a thousand tiny images, though only around 100 of them will be visible at the same time.
I need to load this images from the disk in a separate thread, so that the UI is not blocked and the user can interact with the app while the images are still appearing.
To do so, I've implemented rob mayoff's answer to Proper way to deal with cell reuse with background threads? in Swift as follows, where PotoCell is a subclass of UICollectionViewCell:
var myQueue = dispatch_queue_create("com.dignityValley.Autoescuela3.photoQueue", DISPATCH_QUEUE_SERIAL)
var indexPathsNeedingImages = NSMutableSet()
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoCell
cell.imageView.image = nil
indexPathsNeedingImages.addObject(indexPath)
dispatch_async(myQueue) { self.bg_loadOneImage() }
return cell
}
func bg_loadOneImage() {
var indexPath: NSIndexPath?
dispatch_sync(dispatch_get_main_queue()) {
indexPath = self.indexPathsNeedingImages.anyObject() as? NSIndexPath
if let indexPath = indexPath {
self.indexPathsNeedingImages.removeObject(indexPath)
}
}
if let indexPath = indexPath {
bg_loadImageForRowAtIndexPath(indexPath)
}
}
func bg_loadImageForRowAtIndexPath(indexPath: NSIndexPath) {
if let cell = self.cellForItemAtIndexPath(indexPath) as? PhotoCell {
if let image = self.photoForIndexPath(indexPath) {
dispatch_async(dispatch_get_main_queue()) {
cell.imageView.image = image
self.indexPathsNeedingImages.removeObject(indexPath)
}
}
}
}
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
indexPathsNeedingImages.removeObject(indexPath)
}
However, I'm not getting acceptable results: when I scroll, the UI freezes for a fraction of a second while images are being loaded in the background. Scrolling is not smooth enough. Moreover, while the first 100 images are being loaded, I am not able to scroll at all until the last image has been displayed. The UI is still being blocked after the implementation of multithreading.
Surprisingly, I can achieve the desired smoothness by modifying my queue to:
var myQueue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)
Note that previosly I was using a custom serial queue.
After this change, the UI is fully responsive, but now I have a very serious problem: my app crashes occaionally, and I think it has to do with the fact that several threads may be accessing and modifying indexPathsNeedingImages at the same time.
Trying to use locks/syncronization makes my images end into the wrong cells some times. So, I would like to achieve the smoothness that gives me a global background queue but using a cutom serial queue. I can't figure out why my UI is freezing when I use a custom serial queue and why it is not when I use a global one.
Some thoughts: maybe setting cell.imageView.image = image for around 100 cells takes some time even though image has been already allocated. The problem may be here, since commenting this line makes the scroll way smoother. But what I don't understand is why scrolling is smooth when I leave this line uncommented and I use a global background-priority queue (until the app crashes throwing a message telling that ... was mutated while being enumerated).
Any ideas on how to tackle this?
in this function it should be dispatch_ASYNC to the main queue. The only reason you should switch queues is when getting data off a network or doing a task that takes time and blocks the ui. When asyncing back to the main queue, ui related things should be done.
func bg_loadOneImage() {
var indexPath: NSIndexPath?
dispatch_sync(dispatch_get_main_queue()) {
indexPath = self.indexPathsNeedingImages.anyObject() as? NSIndexPath
if let indexPath = indexPath {
self.indexPathsNeedingImages.removeObject(indexPath)
}
}
if let indexPath = indexPath {
bg_loadImageForRowAtIndexPath(indexPath)
}
}
Also
if let indexPath = indexPath is confusing
After quite some time, I've managed to make it work. Although now I'm testing on an iPad Air 1, and before it was an iPad 2, don't know if this could have some influence on "responsiveness".
Anyway, here's the code. In your UIViewController:
class TestViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewFlowLayout())
var myQueue = dispatch_queue_create("com.DignityValley.Autoescuela-3.photoQueue", DISPATCH_QUEUE_SERIAL)
var indexPathsNeedingImages = NSMutableSet()
...
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(ReuseIdentifiers.testCell, forIndexPath: indexPath) as! TestCell
...
cell.imageIdentifiers = record.imageIdentifiers
indexPathsNeedingImages.addObject(indexPath)
dispatch_async(myQueue) { self.bgLoadOneCell() }
return cell
}
func bgLoadOneCell() {
var indexPath: NSIndexPath?
dispatch_sync(dispatch_get_main_queue()) {
indexPath = self.indexPathsNeedingImages.anyObject() as? NSIndexPath
if let indexPath = indexPath {
self.indexPathsNeedingImages.removeObject(indexPath)
}
}
if let indexPath = indexPath {
loadImagesForCellAtIndexPath(indexPath)
}
}
func loadImagesForCellAtIndexPath(indexPath: NSIndexPath) {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) as? TestCell {
cell.displayImages()
}
}
...
}
And in your UICollectionViewCell subclass:
class TestCell: UICollectionViewCell {
...
var photosView = UIView()
var imageIdentifiers: [String?] = [] {
func displayImages() {
for i in 0..<imageIdentifiers.count {
var image: UIImage?
if let identifier = imageIdentifiers[i] {
image = UIImage(named: "test\(identifier).jpg")
}
dispatch_sync(dispatch_get_main_queue()) {
self.updateImageAtIndex(i, withImage: image)
}
}
}
func updateImageAtIndex(index: Int, withImage image: UIImage?) {
for imageView in photosView.subviews {
if let imageView = imageView as? UIImageView {
if imageView.tag == index {
imageView.image = image
break
}
}
}
}
...
}
I think the only difference between this and what I had before is the call to dispatch_sync rather than dispatch_async when actually updating the image (i.e. imageView.image = image).

iOS CollectionView Help (saving current state)

I am making an iOS app in Swift and I am running into a obstacle I seem to be stuck on. I have a collectionview populated by an string array, which are the names of the images I am populating the image within the collectionview cells:
var tableData: [String] = ["cricket1.png", "cricket1.png", "cricket1.png"]
I've linked up the images to the collectionview with the following code:
//How many cells there are is equal to the amount of items in tableData (.count property)
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tableData.count
}
//Linking up collectionView with tableData
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: CricketCell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CricketCell
cell.imgCell.image = UIImage(named: tableData[indexPath.row])
return cell
}
When I have the user tap the cell, the image goes from cricket1.png to cricket2.png:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("Cell selected")
var cell = collectionView.cellForItemAtIndexPath(indexPath) as! CricketCell
if cell.imgCell.image == UIImage(named:"cricket1.png"){
var cell = collectionView.cellForItemAtIndexPath(indexPath) as! CricketCell
cell.imgCell.image = UIImage(named:"cricket2.png")
}
Now.. here is where I am having trouble. I am currently trying to save data in tableData, however when I do, it always saves it as ["cricket1.png", "cricket1.png", "cricket1.png"]. Even if the image has been tapped and changed to "cricket2.png". Even if all the images on the screen is cricket2.png, when I save tableData, it saves it as ["cricket1.png", "cricket1.png", "cricket1.png"]. I am aware it is because I am storing the variable tableData I declared earlier, but is there any way I can grab a string array of what is on the screen/the current state of the collectionview?
Any help would be appreciated!
Thank you!
You need to update the data in the array yourself. It's independent from image.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("Cell selected")
//1: Get the index for which data in array you need to update
let index = indexPath.row
//I don't think this comparison may server your purpose. When you new an UIImage object, it's a different one than the original image. You may want to just compare the data
let unSelectedImage = "cricket1.png"
if self.tableData[index] != unselectedImage {
//2: Update data in array
var cell = collectionView.cellForItemAtIndexPath(indexPath) as! CricketCell
let selectedImageName = "cricket2.png"
self.tableData[index] = selectedImageName
cell.imgCell.image = UIImage(named: selectedImageName)
}
}
So in this case, even you refresh the table, image will loading according to the new tableData.
I would recommend that u use another property to hold the state of the cell. For example,
var selectedState = [true, false, true, true, true]
Upon tapping the image, update the image and selectedState array accordingly.
For example,
if selectedState[indexPath.row] {
cell.imgCell.image == UIImage(named:"cricket1.png")
else {
cell.imgCell.image = UIImage(named:"cricket2.png")
}
Once you are ready to grab the state of the tapped cells, you can use the selectedState array variable.
You can't use the cell to persist any state as the cell can get dequeued by collectionView and may lost its state as you scroll.
Hope this helps.

Resources