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.
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 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 --
}
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.
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 want to download image asynchronously and load it in UIImageView of cell similar to lazy loading in SWIFT.
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
{
//Creating instance of class UITableViewCell
var cell : CustomTableViewCell!
//Asigning a value to a variable
num = indexPath.row
//Calling a UITableview method dequeueReusableCellWithIdentifier suing self.tableviewcustom in swift
cell = self.tableViewCustom.dequeueReusableCellWithIdentifier(cellIdentifier) as CustomTableViewCell
cell.setView()
var teamdict = DataController.sharedInstance.team.objectAtIndex(indexPath.row) as NSDictionary
cell.label.text = teamdict.valueForKey("name") as NSString
//Creating a instance of UITableview ans also as property
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,uintmax_t()),
{
let url = NSURL.URLWithString(teamdict.valueForKey("logo") as NSString)
let imgData = NSData.dataWithContentsOfURL(url, options: NSDataReadingOptions(), error: nil)
if imgData
{
let image = UIImage(data: imgData)
dispatch_sync(dispatch_get_main_queue(),
{
(self.tableViewCustom.cellForRowAtIndexPath(indexPath) as CustomTableViewCell).teamImgVw.image = image
}
)
}
})
return cell
}
i was able to download and display the image when the table view is scrolled at normal speed but the application crashes when the tableview scrolled faster.
This statement(self.tableViewCustom.cellForRowAtIndexPath(indexPath)) will lead to recursive method call.
dispatch_sync(dispatch_get_main_queue(),
{
(self.tableViewCustom.cellForRowAtIndexPath(indexPath) as CustomTableViewCell).teamImgVw.image = image
})
You're trying to call same method within that method.