Crashes when scrolled faster during asynchronous image download - ios

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.

Related

Table view with images stutters while scrolling

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?

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 --
}

Autolayout, async load image into uiimageview (Swift, xCode6)

Good day! I have 2 problems and I hope you will help me.
1) I have news feed in my application, that contains images.
I am using Autolayout for dynamic cells:
and I want the image to keep its ratio and to completely fill the width of the cell (with margins = 12).
I set constrains, cell is autoresizable, but image didn't save its ratio:
.
What I am doing wrong?
2) The second problem, i load images asynchronously, here is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: EventCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as EventCell
var rowData: NSDictionary = tableData[indexPath.row] as NSDictionary
cell.titleButton.setTitle(rowData["title"] as? String, forState: UIControlState.Normal)
cell.titleButton.addTarget(self, action: "openWebSite:", forControlEvents: UIControlEvents.TouchUpInside)
cell.titleButton.tag = indexPath.row
cell.descriprionLabel.text = rowData["description"] as? String
var urlString = rowData["image"] as String
var image = imageCache[urlString]
if( image == nil ) {
var imgURL = NSURL(string: urlString)
// Download an NSData representation of the image at the URL
var request: NSURLRequest = NSURLRequest(URL: imgURL!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
image = UIImage(data: data) // data -> image
// Store the image in to our cache
self.imageCache[urlString] = image // save in our dictionary
if let cellToUpdate : EventCell = tableView.cellForRowAtIndexPath(indexPath) as? EventCell {
self.table.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
else {
println("Error: \(error.localizedDescription)")
}
})
} else {
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate : EventCell = tableView.cellForRowAtIndexPath(indexPath) as? EventCell {
cellToUpdate.img.image = image
}
})
}
cell.selectionStyle = UITableViewCellSelectionStyle.None;
cell.contentView.setNeedsLayout(); // autolayout bug solution
cell.contentView.layoutIfNeeded(); // autolayout bug solution
return cell
}
All seems okay, but UITableViewCell don't resize when image is loaded and I am trying to reload cell at index path.
Interesting moment, that it will work if I scroll down and then come back to cell.
I have similar error before and I fixed it reading this article UITableView layout messing up on push segue and return. (iOS 8, Xcode beta 5, Swift) , third answer. But it didn't help me now. Looks like I need to call some method to recalculate UITableViewCell, but I don't understand what.
First question : Change UIImageView view mode from Scale to Fill to Aspect Fit (in storyboad)
Second question : Remove dispatch async if image is not nil and make you code look similar like this:
if( image == nil ) {
...
}
else {
cell.img.image = image
}
For first one in the storyboard select the image view and click on the pin icon for setting auto layout constraint and check width and height.
it will remain the width and height constant.

collectionView can't reloadItemsAtIndexPaths at current scene

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.

Setting images in UITableViewCell in Swift

I have a list of reddit posts that I want to display the thumbnail of, if it exists. I have it functioning, but it's very buggy. There are 2 main issues:
Images resize on tap
Images shuffle on scroll
This is the code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Post", forIndexPath: indexPath) as UITableViewCell
let post = swarm.posts[indexPath.row]
cell.textLabel!.text = post.title
if(post.thumb? != nil && post.thumb! != "self") {
cell.imageView!.image = UIImage(named: "first.imageset")
var image = self.imageCache[post.thumb!]
if(image == nil) {
FetchAsync(url: post.thumb!) { data in // code is at bottom, this just drys things up
if(data? != nil) {
image = UIImage(data: data!)
self.imageCache[post.thumb!] = image
dispatch_async(dispatch_get_main_queue(), {
if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
originalCell.imageView?.image = image
originalCell.imageView?.frame = CGRectMake(5,5,35,35)
}
})
}
}
} else {
dispatch_async(dispatch_get_main_queue(), {
if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
originalCell.imageView?.image = image
originalCell.imageView?.frame = CGRectMake(5,5,35,35)
}
})
}
}
return cell
}
This is the app when it loads up - looks like everything is working:
Then if I tap on an image (even when you scroll) it resizes:
And if you scroll up and down, the pictures get all screwy (look at the middle post - Generics fun):
What am I doing wrong?
** Pictures and Titles are pulled from reddit, not generated by me **
EDIT: FetchAsync class as promised:
class FetchAsync {
var url: String
var callback: (NSData?) -> ()
init(url: String, callback: (NSData?) -> ()) {
self.url = url
self.callback = callback
self.fetch()
}
func fetch() {
var imageRequest: NSURLRequest = NSURLRequest(URL: NSURL(string: self.url)!)
NSURLConnection.sendAsynchronousRequest(imageRequest,
queue: NSOperationQueue.mainQueue(),
completionHandler: { response, data, error in
if(error == nil) {
self.callback(data)
} else {
self.callback(nil)
}
})
callback(nil)
}
}
Unfortunately, this seems to be a limitation of the "Basic" table view cell. What I ended up doing was creating a custom TableViewCell. A relied on a tutorial by Ray Wenderlich that can be found here: http://www.raywenderlich.com/68112/video-tutorial-table-views-custom-cells
It's a bit of a bummer since the code is so trivial, but I guess on the bright side that means it's a 'simple' solution.
My final code:
PostCell.swift (all scaffolded code)
import UIKit
class PostCell: UITableViewCell {
#IBOutlet weak var thumb: UIImageView!
#IBOutlet weak var title: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
PostsController.swift
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PostCell", forIndexPath: indexPath) as PostCell
let post = swarm.posts[indexPath.row]
cell.title!.text = post.title
if(post.thumb? != nil && post.thumb! != "self") {
cell.thumb!.image = UIImage(named: "first.imageset")
cell.thumb!.contentMode = .ScaleAspectFit
var image = self.imageCache[post.thumb!]
if(image == nil) {
FetchAsync(url: post.thumb!) { data in
if(data? != nil) {
image = UIImage(data: data!)
self.imageCache[post.thumb!] = image
dispatch_async(dispatch_get_main_queue(), {
if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
postCell.thumb!.image = image
}
})
}
}
} else {
dispatch_async(dispatch_get_main_queue(), {
if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
postCell.thumb!.image = image
}
})
}
}
return cell
}
And my measly storyboard:
I'm not sure the best way to do this, but here a couple of solutions:
Use AFNetworking, like everyone else does. It has the idea of a place holder image, async downloading of the replacement image, and smart caching. Install using cocoa pods, make a bridging file with #import "UIImageView+AFNetworking.h"
Create two different types of cells. Before grabbing a cell with dequeReusableCell... in your cellForRowAtIndexPath, check if it's expanded. If expanded, return and populate an expanded cell otherwise return and populated an unexpanded cell. The cell is usually expanded if it is the 'selected' cell.
Your mileage may vary
it is a huge mistake to call tableView.cellForRowAtIndexPath from within UITableViewDataSource's implementation of tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell. Instead when the async fetch of the thumb image is completed, update your model with the image, then request tableView to reloadRows for that specific cell's indexPath. Let your data source determine the correct indexPath. If the cell is offscreen by the time the image download is complete there will be no performance impact. And of course reloadRows on the main thread.

Resources