How Do I Prevent Reload of UICollectionViewCells in cellForItemAt? - ios

I have a CollectionViewController in which I contact an API to download images based on a set of coordinates. I call this code in the cellForItemAt function at which time it updates the cell's images in realtime with images from Flickr. This works fine.
However, when scrolling up or down, it recalls this code and updates the cells again, when I'd prefer that it look at the existing cells, identify if they have been filled, and simply not run this code.
I have tried implementing logic before the networking code that checks to see if the imageView.images already exist in a local struct I assign them to, but that doesn't seem to work correctly.
Is there a simple method to tell cellForItemAt "for cells where you already have images, don't look for more"?
Here is my current code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath as IndexPath) as! CollectionViewCell
// Get images = using the URL
FlickrClient.sharedInstance().getImagesFromFlickr(latitude: selectedPin.lat, longitude: selectedPin.lon, page: pageCount) { (pin, error) in
if let pin = pin {
let url = pin.images[indexPath.item].imageURL
let data = try? Data(contentsOf: url)
performUIUpdatesOnMain {
cell.imageView.image = UIImage(data: data!)
cell.imageView.contentMode = .scaleAspectFill
}
}
}
return cell
}

Use SDwebImage libray for loading images from url.
https://github.com/rs/SDWebImage
Call Something like this on cell for row :
let url = pin.images[indexPath.item].imageURL
cell.imageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder.png"))

Related

Collection View reusable cell issue while scrolling

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.

How to fill images in to UICollectionView with Alamofire Swift 4

I have a Chat Log which is simply a UICollectionView. Every sell has an avatar (UIImage) and a text bubble. I'm trying to fill avatars with proper images by fetching avatars URL from the server with Alamofire like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "chatMessage", for: indexPath) as! ChatMessageCell
let imageURL = URL(string: messagesArray![indexPath.item].userAvatar)
Alamofire.download(imageURL!).responseData { response in
if let data = response.result.value {
cell.userAvatar.image = UIImage(data: data)
}
}
cell.messageText.text = messagesArray![indexPath.item].userText
}
My problem: In some cells avatar appears in some is not. I think it's related to Alamofire async work. So my question is how to fill images to UICollectionView properly to show each avatar in each cell?
I think it's better to use SDWebImage , as according to your current implementation image fetching happens multiple times
imageView.sd_setImage(with: URL(string: "http://www.example.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
here SDWebImage
UICollectionView reuses existing cells. If you have a collectionView with one million cells there are not going to be one million UICollectionView cells instantiated but they got reused when they scroll out. Now your result callback blocks may set the image of the same cells.
To solve your problem you need to cache the images. There are multiple tutorials about this topic.

iOS load image into table view cell

update
now images displays as they are loaded, but there still a problem I can't solve.
The images overlaps the content of the cell even the three views are in a vertical stack (first is the image, the next two are label views).
I wonder how can a view overlap other views in a stackview
now my talbe view delegate method looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell!
if let contents = contents {
let content = contents[indexPath.row]
if let imageUrl = content.imageUrl, let url = URL(string: imageUrl) {
cell = tableView.dequeueReusableCell(withIdentifier: "ImageContentRow", for: indexPath) as! ContentWithImageTableViewCell
Alamofire.request(url).responseImage(completionHandler: { (response) in
if let image = response.result.value {
cell.imageView?.image = image
cell.setNeedsLayout()
}
})
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "ContentRow", for: indexPath) as! ContentTableViewCell
}
cell.contentTitleVeiw.text = content.title
cell.contentLeadView.text = content.lead
}
return cell!
}
I'm very new to iOS. I've already tutorials and watched video courses and now I would like to implement my very first app.
I try to read a feed and display the content's title, lead and image (if it has any).
I defined a cell, with an UIMageVeiw, and two UILables, at last I embedded them into a vertical StackView (I set distribution to Fill).
Everything works well, but images don't display automatically, just if I click on the cell, or sroll the TableVeiw.
And even if I set the image view to Aspect Fit (and embedded it into the top tof the vertical stack view), when the image displays it keeps it's original size and overlaps the cell content.
I've trying to find out what I do wrong for two days, but I can't solve it.
I try display data like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell!
if let contents = contents {
let content = contents[indexPath.row]
if let imageUrl = content.imageUrl, let url = URL(string: imageUrl) {
cell = tableView.dequeueReusableCell(withIdentifier: "ImageContentRow", for: indexPath) as! ContentWithImageTableViewCell
cell.imageView?.af_setImage(withURL: url)
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "ContentRow", for: indexPath) as! ContentTableViewCell
}
cell.contentTitleVeiw.text = content.title
cell.contentLeadView.text = content.lead
}
return cell!
}
The image view cell's view hierarchy looks like this:
My list looks like this after I start my app and data displayed:
After I click on a cell that should display an image or scroll it out and back the result is this:
At least this is my list view controller with the two prototype cells
(one for contents with image, one for without image)
You need to notify the tableView that the image was loaded and that it has to redraw the cell.
Try changing
cell.imageView?.af_setImage(withURL: url)
to
cell.imageView?.af_setImage(withURL: url, completion: { (_) in
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
})
Take a look at my answer here - in the end you might need to explicitly set height on the imageView.

Slow loading of images in UITableViewController extracted from URL string

I'm trying to load images extracted from the web URL into the image view of each cell.
However, when i scroll the table the screen will freeze as I believe it is attempting to grab the images for each cell 1 by 1.
Is there a way i can make it asynchronous? The resources available out there currently is outdated or incompatible(running obj c) as I'm running on Swift 2
The relevant code I'm using within the table view controller is below :
override func tableView(newsFeedTableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let blogPost: BlogPost = blogPosts[indexPath.row]
cell.textLabel?.text = blogPost.postTitle
let unformattedDate = blogPost.postDate
//FORMATTING: Splitting of raw data into arrays based on delimiter '+" to print only useful information
let postDateArr = unformattedDate.characters.split{$0 == "+"}.map(String.init)
cell.detailTextLabel?.text = postDateArr[0]
let url = NSURL(string: blogPost.postImageUrl)
let data = NSData(contentsOfURL: url!)
cell.imageView!.image = UIImage(data: data!)//WHY SO SLOW!?
print(blogPost.postImageUrl)
return cell
}
Try this
var image: UIImage
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {() -> Void in
// Background thread stuff.
let url = NSURL(string: blogPost.postImageUrl)
let data = NSData(contentsOfURL: url!)
image = UIImage(data:data)
dispatch_async(dispatch_get_main_queue(), {() -> Void in
// Main thread stuff.
cell.imageView.image = image
})
})
Lets clean your code a bit. First of all, you are trying to declear ALL your cells in your viewController. That means your app is not trying to load every image one byt one, but more like everything all together.
You need to create a separate file called PostCell what is going to be a type of UITableViewCell.
Then you need to go to your prototype cell and connect those view elements to that PostCell just like you would add those to any other ViewController.
Now, here is new code to your cellForRowAtIndexPath function:
override func tableView(newsFeedTableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let blogPost = blogPosts[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? PostCell {
cell.configureCell(blogPost)
return cell
}
return UITableViewCell() // You need to do this because of if let
}
And declear this on that PostCell:
func configureCell(post: BlogPost) {
this.textLabel.text = post.postTitle
let postDateArr = unformattedDate.characters.split{$0 == "+"}.map(String.init)
this.detailTextLabel.text = postDateArr[0]
// I would add few if let declarations here too, but if you are sure all these forced ! variables do exciest, then its ok
let url = NSURL(string: blogPost.postImageUrl)
let data = NSData(contentsOfURL: url!)
this.imageView.image = UIImage(data: data!)
}
Or something along those lines. When you connect those elements to your cell, you will get proper variable names for those.
That SHOULD help. There are plenty of tutorials how to make a custom tableviewcell. Some of them advice to put all the declarations inside that cellForRowAtIndexPath, but I have found that it get's problematic very fast.
So...my advice in a nutscell...create a custom tableviewcell.
Hope this helps! :)
To load the image on every cell use SDWebImage third party library. You can add it using pods as put pod 'SDWebImage' It provides various methods to load the image with caching or without caching asynchronously. With caching you don't really need to worry about loading image data every time cell appears on the screen. Try this
override func tableView(newsFeedTableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? PostCell {
--reset your cell here--
// cell.imageView.image = nil
}
cell.imageView.sd_setImageWithURL(YOUR_URL, placeholderImage: UIImage(named: "")) {
(UIImage img, NSError err, SDImageCacheType cacheType, NSURL imgUrl) -> Void in
// Do awesome things
}
-- configure your cell here --
}

UICollectionView queries with dequeuing

I have an UICollectionView that makes a grid. Each cell to make the grid has a UIImage in it (created in IB).
I am using reuseable cells to keep the requests down.
How can I use this cell and the UIImage ? Is there someway of storing it in an array before it goes away ? I have created a tag but I don't if this will help ? If i created each cell manually then there will be around 100 #IBOutlets in my controller !! Here is my code to get the cells displayed..
Any ideas would be brilliant. I am trying to get the UIImage inside the cell, so I can hide it and also name it before the cell dequeues.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("LetterCell", forIndexPath: indexPath) as UICollectionViewCell
cell.tag = indexPath.row
return cell
}
Images take up a surprising amount of memory. As a result, you generally do not want an architecture that requires you to hold all of images (or worse, cells) in memory at one time. You want your collection view cells to be reused and you want to retrieve the images from persistent storage in a just-in-time manner (aka, "lazy" image loading).
To minimize the memory footprint of your app, so your model would generally contain the minimal amount of information, for example just references to those images (such as filenames). Only load the images only when they're really needed by the UI.
For example, let's assume that the images were files in the “Application Support” folder of the device, then you might have an array of filenames (called imageNames in my example below), and you might do something like:
var imageNames = [String]() // this is populated elsewhere, perhaps `viewDidLoad`
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CustomCell
let imageURL = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(imageNames[indexPath.item])
cell.imageView.image = UIImage(contentsOfFile: imageURL.path)
return cell
}
If you really wanted to hold those images in memory (for example, for even smoother response time), you might use a NSCache, but make sure this cache empties itself upon receiving memory pressure. For example:
var imageCache = ImageCache()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CustomCell
let imageName = imageNames[indexPath.item]
if let image = imageCache[imageName] {
cell.imageView.image = image
} else {
let imageURL = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(imageName)
let image = UIImage(contentsOfFile: imageURL.path)
imageCache[imageName] = image
cell.imageView.image = image
}
return cell
}
Where
class ImageCache: NSCache<NSString, UIImage> {
var observer: NSObjectProtocol?
override init() {
super.init()
// empty queue upon memory pressure
observer = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main) { [unowned self] notification in
self.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
subscript(key: String) -> UIImage? {
get { return object(forKey: key as NSString) }
set { setValue(newValue, forKey: key) }
}
}
There are other optimizations that one might consider, too. For example, if these images are large, you might make sure that you're loading the image view with images resized to something optimal for the collection view cell. But hopefully this illustrates some of the basic concepts when dealing with images in a UICollectionView.

Resources