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.
Related
I am using the collection view to show the gif's on the list.
Now facing the cell reusable issue while scrolling the cells up or down of collection view.
Like itemA is on first place in the list and itemB is on the second place in the list.
but when I scroll the data in the collection view. the places of items got misplaced. like some time itemA gone on 5th place or sometimes anywhere in the list.
i know i think this is the use with reusable cell, but don't know how to salve this.
Plss help.
Collection view cellForItemAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GifCell", for: indexPath as IndexPath) as? GifCell else {
fatalError()
}
if gifArr.count > 0 {
let urlString = self.gifArr[indexPath.row]
let url = URL(string: urlString)!
DispatchQueue.global().async {
let imageData = try? Data(contentsOf: url)
let imageData3 = FLAnimatedImage(animatedGIFData: imageData) // this is the 3rd pary library to show the gifs on UIimageview's
DispatchQueue.main.async {
cell.imageView.animatedImage = imageData3
cell.textLabel.text = String(indexPath.row)
}
}
}
return cell
}
In GifCell you could implement prepareForReuse() method:
Performs any clean up necessary to prepare the view for use again.
override func prepareForReuse() {
super.prepareForReuse()
imageView.animatedImage = nil
textLabel.text = ""
}
Note:
at this point, each time cellForItemAt method gets called, the url will be reloaded, so later, you might want find a way to cache the images instead of keep reloading them.
First solution: You can cache data and every time check if there is, use your cache.
you can use this link, but replace UIImage with gift type!
or
try this, I did not test it
if let giftAny = UserDefaults.standard.value(forKey: "giftUrl") {
//cast giftAny to Data
// use cached gift
} else {
// cache gift
let giftData = try? Data(contentsOf: url)
UserDefaults.standard.setValue(giftData, forKeyPath: "giftUrl")
//use gift
}
Second Solution: Don't reuse cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = UICollectionViewCell(style: .default, reuseIdentifier:"Cell")
return cell
}
but in this case, if you have many cells, memory leak is unavoidable.
I have a table view which loads custom table view cells. In the table view cell I have an image view, among other controls. The problem is that the table view stutters while scrolling, even though the images are being read from Cache or documents directory (using either isn't making any difference)
Here is the code for displaying the table view cell:
func tableView(tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath) {
if cell.isKindOfClass(EventsTableViewCell) {
let eventCell = cell as! EventsTableViewCell
let list = self.eventsList
if list.count > 0 {
let event = list.objectAtIndex(indexPath.row) as! Event
//other cell labels configured here
//imageCache is NSCache instance
if let imageData = self.imageCache.objectForKey(event.name) as? NSData {
let image = UIImage(data: imageData)
eventCell.eventImageView.image = image
}
else if event.imageUrl != "" {
eventCell.setImageToCell(event)
}
}
}
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "eventCell"
var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier)
if cell == nil {
let nibCollection = NSBundle.mainBundle().loadNibNamed("EventsTableViewCell",
owner: nil,
options: nil)
cell = (nibCollection as NSArray).objectAtIndex(0) as? EventsTableViewCell
}
cell?.selectionStyle = .None
return cell!
}
In EventsTableViewCell,
func setImageToCell(event: Event) {
//apply placeholder image. This image will be replaced if an image is found for the particular event.
if event.image != nil {
dispatch_async(dispatch_get_main_queue(), {
self.eventImageView.image = event.image
})
}
}
So even though none of the images are being downloaded here, the table view still can't handle the scrolling properly. I am not sure what I am doing wrong. Can the size of the images be an issue here?
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
}
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).
I have a UITableView with UICollectionView insight every table view cell. I use the UICollectionView view as a gallery (collection view with paging). My logic is like this:
Insight the method
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// This is a dictionary with an index (for the table view row),
// and an array with the url's of the images
self.allImagesSlideshow[indexPath.row] = allImages
// Calling reloadData so all the collection view cells insight
// this table view cell start downloading there images
myCell.collectionView.reloadData()
}
I call collectionView.reloadData() and in the
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// This method is called from the cellForRowAtIndexPath of the Table
// view but only once for the visible cell, not for the all cells,
// so I cannot start downloading the images
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoCollectionCell
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
if indexPath.item < arr.count {
var imageName:String? = arr[indexPath.item]
if let imageName = imageName {
var escapedAddress:String? = imageName.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
if let escapedAddress = escapedAddress {
var url:NSURL? = NSURL(string: escapedAddress)
if let url = url {
cell.imageOutlet.contentMode = UIViewContentMode.ScaleAspectFill
cell.imageOutlet.hnk_setImageFromURL(url, placeholder: UIImage(named: "placeholderImage.png"), format: nil, failure: nil, success: nil)
}
}
}
}
}
}
return cell
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
println("collection row: \(collectionView.tag), items:\(arr.count)")
return arr.count
}
}
return 0
}
I set the right image for the cell. The problem is that the above method is called only for the first collection view cell. So when the user swipe to the next collection view cell the above method is called again but and there is a delay while the image is downloaded. I would like all the collection view cells to be loaded insight every visible table view cell, not only the first one.
Using the image I have posted, "Collection View Cell (number 0)" is loaded every time but "Collection View Cell (number 1)" is loaded only when the user swipe to it. How I can force calling the above method for every cell of the collection view, not only for the visible one? I would like to start the downloading process before swiping of the user.
Thank you!
you're right. the function func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell will be called only when cell start to appear. that's a solution of apple called "Lazy loading". imagine your table / collection view have thousand of row, and all of those init at the same time, that's very terrible with both memory and processor. so apple decide to init only view need to be displayed.
and for loading image, you can use some asynchronous loader like
https://github.com/rs/SDWebImage
it's powerful and useful too :D