Update UITableview cell with new loaded content while scrolling - swift

I have a UITableView, I use the cellForRowAtIndexPath: function to download an image for a specific cell. The image download is performed asynchronously and finishes after a short amount of time and calls a completion block after finishing. I set the cell content to the downloaded image inside the completion block, but it doesn't get updated until after the user finishes scrolling. I know that the reason is because the code should be running in NSRunLoopCommonModes, but I don't know how to do that in this case, any help is appreciated.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let dequeued = self.tableView.dequeueReusableCellWithIdentifier(imageFileCellIdentifier)
let cell = dequeued as! ImageTableViewCell
source.FetchImage(imageLocation, completion: { (image) in
dispatch_async(dispatch_get_main_queue(), {
cell.previewImage.image = image
cell.previewImage.contentMode = .ScaleAspectFill
}
})
}

try this
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let dequeued = self.tableView.dequeueReusableCellWithIdentifier(imageFileCellIdentifier)
let cell = dequeued as! ImageTableViewCell
source.FetchImage(imageLocation, completion: { (image) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.previewImage.image = image
cell.previewImage.contentMode = .ScaleAspectFill
})
})
}

UI must be updated in main thread
You can use something like this to update imageView - coz asynchronous completion block may not call within main thread.
dispatch_async(dispatch_get_main_queue(), {
cell.previewImage.image = image
cell.previewImage.contentMode = .ScaleAspectFill
}
to update UIs
And also it is better if you check that the cell you are updating is the correct cell. Because when download completed(asynchronously) cell may be scrolled-out of the view and that cell may be allocated to another row. In that case you are setting the image to a wrong cell.
eg.
if cell.imageName == 'downloaded_image_name' {
// do update
}

Related

Swift TableView making multiple network requests for images

I have a table in swift. Whenever I scroll up and down the tableview it runs the getImageForCell function again even though the image has already been loaded. Is there a way for this not to happen. Below is my code.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReviewTableViewCell", for: indexPath) as! ReviewTableViewCell
let review = json[reviewType][indexPath.row]
cell.nameLabel.text = review[userType]["name"].stringValue
cell.reviewLabel.text = review["message"].stringValue
cell.dateLabel.text = review["created_at"].stringValue
cell.ratingStars.rating = Double(review["rating"].intValue)
getImageForCell(url: review[userType]["photo_url"].stringValue, cell: cell)
return cell
}
func getImageForCell(url: String, cell: ReviewTableViewCell) {
Alamofire.request(url).responseImage { response in
if let downloadedImage = response.result.value {
print("downloaded image \(downloadedImage)")
DispatchQueue.main.async {
cell.profileImageView.image = downloadedImage
}
}
}
}
This is due to both not cacheing the image, and also not reusing the cell - in your view you are recreating the cell each time - this would not specifically fix the image issue, but will improve performance. My favourite cache image extension is kingfisher (no affiliation), although alamofire can be used to cache images too.
https://github.com/onevcat/Kingfisher

Multiple UITableViewCells each with UIProgressView very slow to update

I am making a game which has 10 UITableViewCells in a UITableView. Each of the 10 UITableViewCells has one UIProgressView plus a lot of other views. I update the UITableView every 1/10th of a second, this is very slow and lags on older devices. I update it every 1/10th second for UX, gives a smooth progress view feel to game.
Is there a way to just update just the Progress Views in each cell individually rather than having to call the tableView.reloadData() that will update all the views in each cell?
Code example:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "businessCell", for: indexPath) as! BusinessCell
cell.progress.progress = Float( businessArray[indexPath.row-1].getCurrentProgress() )
//lots of other views are updated here
return cell
}
}
could I maybe change this line:
cell.progress.progress = Float( businessArray[indexPath.row-1].getCurrentProgress() )
to something like this:
cell.progress.progress = someVarLocalToViewControllerContainingTableView[indexPath.row]
and when I update this local var it would update only the progressView or something?
I have tried many ways but cannot figure out how to do this...
If you need to update a progress for a specific cell, then call this
func reloadProgress(at index: Int) {
let indexPath = IndexPath(row: index, section: 0)
if let cell = tableView.cellForRow(at: indexPath) as? BusinessCell {
cell.progress.progress = Float( businessArray[index - 1].getCurrentProgress() )
}
}
If you need to reload all bars in the table:
func reloadProgress() {
for indexPath in tableView.indexPathsForVisibleRows ?? [] {
if let cell = tableView.cellForRow(at: indexPath) as? BusinessCell {
cell.progress.progress = Float( businessArray[indexPath.row - 1].getCurrentProgress() )
}
}
}
you can use :
self.tableView .reloadRows(at: <[IndexPath]>, with: <UITableViewRowAnimation>)
rather than using tableView.reloadData()
Please check below link:
Is it possible to refresh a single UITableViewCell in a UITableView?
It might help in your scenario.

UICollectionView Not Updated with custom UIViewController

I created a custom UIViewController and extended it to implement: UICollectionViewDataSource, UICollectionViewDelegate. In story board, I added UICollectionView and made reference from my controller to this UICollectionView (and setup the delegates for data source and collection view delegate).
I obviously implemented the minimal requirements for these 2 delegates. Now, since i sometimes asynchronously load images, I call cell.setNeedsLayout(), cell.layoutIfNeeded(), cell.setNeedsDisplay() methods after image download done (cell.imageView.image = UIImage(...)). I know all these methods are called. However, the images on the screen were not updated.
Did I miss something? Thank you!
Edit: Add Sample code - how update was called -
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
....
let cellImage = ServiceFacade.sharedInstance.getImageFromCaching(image!.url)
if cellImage == nil {
// set image to default image.
cell.cellImage.image = UIImage(named: "dummy")
ServiceFacade.sharedInstance.getImage(image!.url, completion: { (dImage, dError) in
if dError != nil {
...
}
else if dImage != nil {
dispatch_async(dispatch_get_main_queue(),{
let thisCell = self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
thisCell.cellImage.image = dImage
thisCell.setNeedsLayout()
thisCell.setNeedsDisplay()
thisCell.layoutIfNeeded()
})
}
else{
// do nothing
}
})
}
else {
cell.cellImage.image = cellImage!
cell.setNeedsDisplay()
}
// Configure the cell
return cell
}
Chances are the UICollectionView has cached an intermediate representation of your cell so that it can scroll quickly.
The routine you should be calling to indicate that your cell needs to be updated, resized, and have it's visual representation redone is reloadItemsAtIndexPaths with a single index path representing your cell.

Move activity indicator dynamically in UITableView in Swift 2.0

I am aware of adding the activityIndicator to a certain place in the view, starting and stopping the animation when required. But in certain instances in my app, where the user clicks on a cell(with the title and subtitle), I fetch some data in the background and then show an alert to the user to get some inputs. In all these cases, I just display an activity indicator in the middle, which doesn't look so good. So, I would like to display the activity indicator only at the end of the cell, on which the user clicked. Is there a way to do that?
One thing I could think of is using a custom table View Cell and then explicitly adding an activity indicator there. But is it possible to do the same with a standard title and subtitle cell, and may be dynamically re-positioning the activity indicator based on the position of the cell the user has clicked?
I think you can do it using UITableViewCell's accessoryView
var selectedIndexPath : NSIndexPath?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// ...
if (indexPath == selectedIndexPath)
{
cell.accessoryView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.Gray)
(cell.accessoryView as! UIActivityIndicatorView).startAnimating()
}
else
{
cell.accessoryView = nil
}
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}

How to change an UIImageView into a custom cell

I have an UIImageView in a custom cell (#IBOutlet var imageSquadra: UIImageView!) and in the TableViewController I initialize this imageView:
let squadra = DataManager.sharedInstance.arrayCori[indexPath.row]
cell.imageSquadra.image = squadra.sfondo1
Now I need to change the image touching up inside the cell with an other image, and return to the previous image touching up into an other cell.
Is it hard to do?
EDIT: or if was easier, would be fine apply a CAFilter like a shadow to the image.
On your tableview set multiple selection to NO:
tableView.allowsMultipleSelection = false
Then you want something like this on your TableViewController
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Store which indexPath has been selected in a global variable
tableView.reloadData()
}
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
tableView.reloadData()
}
In your cellForRow method you want something like...
cell.useSelectedImage(indexPath == storedIndexPath)
And finally in your custom cell class you want to add a method that does similar to this
func useSelectedImage(bool:useSelected)
{
self.myImage = useSelected ? "selectedImage" : "unselectedImage"
}
My Swift is a bit rusty... but you should be able to get the general idea from what I've done here