Self Sizing Cells With Asynchronous ImageLoading - ios

Update: This Github link contains the project. If possible download and tap on Reload on top you'll get to know the difference. and Mr. Zhang thank you for "Aspect Scale to Fill" suggestion. it helped me little.
I've a tableview which loads images Asynchronously (with AFNetworking UIImageView Category Class).Images are compressing for first time on visible cells. Once I reload again or scroll op and down Images are adjusting according to its size.
Basically I'll get different size Images. Which width should be cell width, height can be anything
Implemented all necessary steps for selfsizing(proper autolayout constraints and proper delegate methods)
//my tableview setup
self.tblTest?.estimatedRowHeight = 1000.0 //i've tried with all possible guesses and
//tried with delegate also
self.tblTest?.rowHeight = UITableViewAutomaticDimension
I've added constraints as below:
in my test cell.
func setUpImage(url:NSURL){
//tried with another function also without NSURLRequest
self.imvTest?.setImageWithURLRequest(NSURLRequest(URL: url),
placeholderImage: UIImage(named: "store_placeholder"),
success: { (requset, response, image) -> Void in
self.imvTest?.image = image
print(image.size)
}, failure: { (error) -> Void in
self.imvTest?.image = nil
})
}
Result
Expected
How can i prevent image squeezing for the first time??
P.S: I don't think its correct to reload table view each and time image is being set.
Please suggest me what i've been missing and any content priorities etc.,??

Try calling
tableView.beginUpdates()
tableView.endUpdates()
after setting the image in the success closure
Or try tableView.reloadRowsAtIndexPaths with the indexPath of the cell after setting the image

What I believe is happening is that the cell gets laid out with its original content size. Then the new image asynchronously comes in and gets set. Typically you set the data for the cell in cellForRowAtIndexPath:, which occurs before cell layout. However when an image comes in asynchronously, layout has already happened, so it's too late.
When a new image comes in, the data has changed, so you need to tell the tableview to reload the data.
Instead of reloading the tableview as a whole however, you can reload an individual cell when the image comes in. This will of course be much more performant than reloading the entire tableview.

Related

Cancel request for Alamofire image

Basic Information:
I have a tableView and each tableView cell containing an image and few labels.
I use tableView prefetch data method to make API calls to fetch data as the tableView scrolls.
Issue:
Sometimes I see a particular cell showing an image of some other cell.
This happens when I scroll a little faster than normal speed.
Note: In order to overcome from this issue I just need to scroll the tableView up/down
Things tried:
I have set the imageView to nil in prepare for reuse method.
The conclusion I have reached:
By debugging the issue I understood that once a cell is visible I make an image request using Alamofire Image but I scroll it before we received the response.
So, what might be happening is that on receiving a response it is setting an image for the cell but that cell is not visible as I am reusing the cells. The cell contains some other data.
Please let me know how can I cancel the request if the cell is not visible.
Let me know if I am missing something in the question.
What I do in this situation is set the cell's tag to match the row number.
When the image requests complete, I check if it still has the same tag (if it was reused, tag would be changed).
Something like this, in cellForRowAt indexPath:
cell.tag = indexPath.row
ImageService.shared.getImage(completion: { (image) in
if let image = image, cell.tag == indexPath.row {
//apply image
}
}
This doesn't cancel the request, but it ignores the result if the cell is not in the same position anymore.
This is happening because tableView reuse the cells and if it having a image downloading in queue and you scroll and the downloading is complete, it shows the downloaded image for few second until correct image download.
use placeholder in af_setImage method. it will solve your problem
imageView.af_setImage(withURL: URL(string: img)!, placeholderImage: UIImage(named: "product_placeholder")

How to display a placeholder before the tableview cells get displayed?

This questions is referencing just before a tableview fetches its data and displays its cells.
I've seen a few apps lately display a rough outline type image of the tableview cells for the brief moments before the populated cells get displayed.
How is this done?
Is a placeholder image used for the entire tableview or are placeholder type images rendered for each cell until the cell is dequeued?
Here are examples from Facebook and the fiverr app
Create a separate UITableViewCell class where the content of the cell is a UIImageView that has some kind of placeholder image of what your cells will look like. Populate the UITableView with those cells while your background request is being made. When the request completes, start a table update in which you remove all the placeholder cells, then insert all the "real" cells.
According to me it would be better to add backgroundView to tableView.
write:
while searching /fetching data:
if results.count == 0{
tableview.backroundView = emptyBlurView
}
once data is received so before reload :
tableview.backroundView = nil
when start requesting on server show the place holder cell and network response are received show the data container cell. using a placeholder cell same as activity indicator.
FaceBook are using simmer effect for its placeholder cell.
Pod
https://github.com/malkouz/ListPlaceholder
https://github.com/Juanpe/SkeletonView

adding subview in a single uitableviewcell

Im facing a problem with adding a UIImageView to a single UITableViewCell. I add the subview like this in the cellForIndexPath delegate method which is ONLY added to the cell if the self.mediaTypeArray contains the string: "Image" at the index: indexPath.row
let cell = self.timelineTableView.dequeueReusableCellWithIdentifier( NSStringFromClass(ClassicCaseCell), forIndexPath: indexPath) as? ClassicCaseCell
if self.mediaTypeArray[indexPath.row] == "Image" {
let imageView = UIImageView()
imageView.frame.size = (cell?.evidenceView.frame.size)!
imageView.frame.origin = CGPointZero
imageView.image = img
cell?.evidenceView.addSubview(imageView)
}
Which it works great however, I find that every other two cells contains that same imageView even when self.mediaTypeArray[indexPath.row] is NOT equal to "Image" and I don't understand why. I think it may have something to do with reusable tableviewcells but I still don't see why it would do this. Please help!
I'm getting the feeling that this is because of cells being re-used.
A way you can work around this is to override your ClassicCaseCell's prepareForReuse method to remove any image view from its evidenceView.
I would however recommend that you don't add the image view in this fashion (through the delegate's cellForIndexPath method). Instead, your ClassicCaseCell should hold the image view as an instance variable which you can then set in cellForIndexPath. This way, there is only at most one. You can also make sure to set the image view to be nil in prepareForReuse, making sure that it won't appear in the cell if it is not set.
First, don't use tag property as recommended elsewhere. That was a technique used a long time ago, but Apple discourages that practice now. Second, I'd suggest you simplify your life and simply don't programmatically add image views in cellForRowAtIndexPath. If you programmatically add image views, cell reuse introduces a clumsy process of determining whether (a) you need an image view; (b) whether there is an existing image view; and (c) possibly adding/removing image view and/or getting reference to existing one.
One very simple solution is to just have two cell prototypes, one with an image view and another without. Then, based upon the media type, dequeue a cell with the appropriate storyboard identifier and use it.
The other alternative is to have the image view in the cell regardless, and hide/show it as appropriate. The challenge then becomes how to best manage two sets of the constraints, one for when the image view is visible and one when it's not. You can do this with judicious choice of constraint priorities, activating/deactivating the appropriate constraints in cellForRowAtIndexPath, etc. It can be done, but this is more cumbersome than the above approach, whereby you just employ two cell prototypes.
You only need to add the UIImageView once so if the cell is re-used again, it (might) already be there. Your problem is to detect if you've already added it or not. Here are a couple suggestions:
1) ALWAYS create it (and just don't set the image, or hide it)
2) assign it a unique tag and look for the tag when you need to set it... no tag, then create it
Override prepareForReuse delegation in your tableview cell and remove imageview from there

iOS8 change height of dynamic cell "now", re internal cell content

Here's a dynamic cell
Note - in the example, the text is not data driven. It's just some text local to the cell (consider, say, a help text). At runtime, change the .text of the UILabel from one word to many lines, using a button actually inside the cell. iOS perectly resizes the cell and table....
... but only when the cell is scrolled offscreen, and then on again.
How to alert the table view to recalculate everything "now" ?
(Please note, this question ONLY in the case of iOS8+, Xcode7+, autolayout for dynamic cell heights.)
Changing height
So basically, there are two ways to do:
The first one is to actually reload the cell (not the tableview). Reloading will call new heightForRow (don't forget to purge cache, if you are caching the sizes), which will return proper new height:
let indexPaths = [NSIndexPath(forRow: ~the rows in question~, inSection: 0)]
self.table.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
(Note however that this often involves reloading more than one row; notably if you select/deselect, you have to reload all rows changed.)
If however you ONLY want to change the size of the cell and the content per se, and did not really change the data content ... so for example:
you clicked some button and you assigned new local text in the cell to outlets (perhaps a help text):
you changed only the LAYOUT of the cell. for example, you made a font larger, or changed the margin of a block of text so that the height of a block of text changed, so indeed the height of the overall cell changed:
In that case instead of reloading, just call the following, which forces the tableview to basically do all animations, and for that it needs new heights, so it requests it:
self.table.beginUpdates()
self.table.endUpdates()
The true solution
I see what your problem is. You are trying to change the height of the cell from the actual cell - but you will not succeed in that -> and you should not. See, the cell is view, and view should not have any idea about its data whatsoever - view is what presents. If you need any changes, you should inform your controller to do so. To do that, you can use notifications, but preferably protocols / delegates.
So at first you create protocol in your cell, which will be used to inform the controller, that there is a change:
protocol MyCellDelegate {
func buttonTappedForCell(cell : UITableViewCell)
}
Now, you need to conform to that protocol in your view controller that contains table:
class MyClassWithTableView : MyCellDelegate
Lastly, you need to declare delegate in the cell:
class MyCell {
var delegate : MyCellDelegate
}
And assign it in the configuration of the cell, which you probably have in the view controller:
cell.delegate = self
This is the basic setup for all the delegates / protocols really, and now, when you click on your button, you can forward the action to your controller:
#IBAction myButtonTouchUpInside() {
self.delegate.buttonTappedForCell(self)
}
After doing all that, proceed as in part 1. That is to say, either reloadRowsAtIndexPaths or a beginUpdates / endUpdates pair as explained above.
Hope it helps!
I'm presuming you're not setting the text property of the UILabel inside cellForRowAtIndexPath but rather somewhere else (or doing it asynchronously). If that's the case, I wouldn't update the UI there. Rather, I'd update the model backing the table and then call reloadRowsAtIndexPaths. That will let cellForRowAtIndexPath call again, but unlike reloading the whole table, this will gracefully keep the contentOffset of the tableview right where it is.
I know this all sounds unnecessarily complicated, but the bottom line is that you don't own this view, the table view does. It has to do all sorts of stuff above and beyond updating the cell. I.e., if the cell grew, figure out which cells scrolled out of view and dequeue them. If the cell shrunk, figure out which cells scrolled into view as a result.
It's a surprisingly complex dance. You can try calling setNeedsLayout on the cell, but I wouldn't expect that to work (and even if it does, it is a fragile approach). The table view is responsible for managing its cells, so if you really should just update model and reload that one cell.
did you try calling reloadRowsAtIndexPaths on the cell index? it's supposed to animate to the new size, if the constraints are setup correctly.
You should call self.tableView.reloadData() just AFTER you made the cell's label's text change.
It will force the tableView to redraw the cell's. That's what happened when you scroll, the cell is being reused, and redrawn when it comes back again.
EDIT:
If you can't or won't do a reloadData on your tableView, you can use:
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(row:0 section:0)] withRowAnimation:UITableViewRowAnimation.Automatic)
self.tableView.endUpdates()
I dont know your code but did you really execute your ui changes on the main thread. Same problem happened to me and was solved with putting the exectuion on the main thread.
dispatch_async(dispatch_get_main_queue()) { () -> Void in
[...]
}

UIWebImage+ AFNetworking images shifting in cells

I have a collection view. In the cellForRow method I set an object in my collectionViewCell. The setter for that object takes the object's image url and downloads the image asynchroneously using AFNetworking. (the setter is in the custom cell subclass )
However when I download new images and i reloadData, I have to scroll down for the proper images to load... And sometimes the images change cells or duplicate (but I know the object in that cell doesn't change because the object title is the expected one.
What is happening?
If you want the image to swap out without reloading the cell (or scrolling down) you will need to call:
[self.imageView setImageWithURL:<#some url#> placeholderImage:<#placeholder image same size as imageview#>];
AFNetworking takes care of caching images so you can be safe to call the same setter every time cellForRow is called (to avoid image reuse on different cells)
NB: Its important the placeholder image is the same size as the imageView - your image will be swapped out.
I think this could be mainly because of the cell being reused bu the collectionview. Try setting the image property of your custom cell to nil right after dequeueReusableCellWithReuseIdentifier:

Resources