Dynamic cell height with async images in Swift 2 - swift

I'm trying to build a Facebook-like feed. I'm using a standard Tableview with autolayout constraints set up in storyboard.
The width should be 100% of the viewport and the height according to the aspect ratio of the image.
I'm using Haneke's hnk_setImageFromURL for async image loading from the server.
I saw here the option to use tableview.beginUpdates() or endUpdates() and it causes the first render to work alright, but once I start scrolling the height calculation goes wrong, cells appear blank and/or the app crashes due to indexOutOfBounds.
Is manually calculating the height in heightForRowAtIndexPath with pre-given aspect ratio from the server the only option?
My relevant part of the code
ViewController:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 500
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FeedItem", forIndexPath: indexPath) as! FeedItemTableViewCell
content.hnk_setImageFromURL(NSURL(string: contentImage)!, placeholder: nil, format: nil, failure: nil, success: {image in
cell.content.image = image
self.beginUpdates()
self.endUpdates()
}
)
}
P.S. A side effect caused by using Haneke is that I have to provide some initial size to the UIImageView, currently I'm using a placeholder image which causes irritating animations when the image is loaded from the server

Here instead of loading cell data in the cellForRowAtIndex method,I have created a subclass of TableViewCell. And I have used SDWebImage instead of Haneke.
Hope this helps

Related

TableView calculates wrong estimatedHeightForRowAt

I'm making a chat like application, where the tableView displays dynamic height cells.
The cells have their views&subviews constrained in the right way
So that the AutoLayout can predict the height of the cells
(Top, Bottom, Leading, Trailing)
But still - as you can see in the video - the scroll indicator bar shows that wrong heights were calculated:
It recalculates the heights when a new row is appearing.
Video: https://youtu.be/5ydA5yV2O-Q
(On the second attempt to scroll down everything is fine)
Code:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
It is a simple problem. Can someone help me out?
Update 1.0
Added github:
https://github.com/krptia/Test
But still - as you can see in the video - the scroll indicator bar shows that wrong heights were calculated:
So what you want is precise content height.
For that purpose, you cannot use static estimatedRowHeight.
You should implement more correct estimation like below.
...
var sampleCell: WorldMessageCell?
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "WorldMessageCell", bundle: nil), forCellReuseIdentifier: "WorldMessageCell")
sampleCell = UINib(nibName: "WorldMessageCell", bundle: nil).instantiate(withOwner: WorldMessageCell.self, options: nil)[0] as? WorldMessageCell
}
...
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let cell = sampleCell {
let text = self.textForRowAt(indexPath)
// note: this is because of "constrain to margins", which value is actually set after estimation. Do not use them to remove below
let margin = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20)
// without "constrain to margins"
// let margin = cell.contentView.layoutMargins
let maxSize = CGSize(width: tableView.frame.size.width - margin.left - margin.right,
height: CGFloat.greatestFiniteMagnitude)
let attributes: [NSAttributedString.Key: Any]? = [NSAttributedString.Key.font: cell.messageLabel.font]
let size: CGRect = (text as NSString).boundingRect(with: maxSize,
options: [.usesLineFragmentOrigin], attributes: attributes, context: nil)
return size.height + margin.top + margin.bottom
}
return 100
}
This is too precise (actually real row height) and maybe slow, but you can do more approximate estimation for optimization.
You need to set tableFooterView to empty.
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
// your staff
}
According to your answer on my comment that when you set
estimatedHeightForRowAt and heightForRowAt the same values it does
work
I can confirm that you are right and that there is the problem that AutoLayout cannot calculate the right value for estimatedHeightForRowAt. So basically there are two possible things to do:
find alternative layout that will produce better results
make your own calculation for estimatedHeightForRowAt which will produce more accurate results (in general you should be able to tell what is expected height per text length and then add margins to that figure - you need to put a bit of effort to find the proper math, but it should work).
The problem is with your estimatedHeightForRowAt method. As the name implies it gives the estimated height to the table so that it can have some idea about the scrollable content until the actual content will be displayed. The more accurate value will result in a more smooth scrolling and height estimation.
You should set this value to big enough so that it can represent the height of your cell with the maximum content. In your case 650 is working fine.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 650
}
The result would be far better with this approach.
Also, there is no need to implement delegate method for height until you want a variation on index bases. You can simply set table view property.
tableView.estimatedRowHeight = 650.0
tableView.rowHeight = .automaticDimension
Optimization
One more thing I noticed in your demo project. You've used too many if-else in your cellForRowAtIndexPath which is making it little slower. Try to minimize that. I've done some refinement to this, and it improves the performance.
Define an array which holds your message text.
var messages = ["Lorem ipsum,"many more",.....]
Replace your cellForRowAt indexPath with below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell : WorldMessageCell
cell = tableView.dequeueReusableCell(withIdentifier: "WorldMessageCell", for: indexPath) as! WorldMessageCell
if indexPath.row < 14 {
cell.messageLabel.text = messages[indexPath.row]
}
else if indexPath.row >= 14 && indexPath.row != 27 {
cell.messageLabel.text = messages[14]
}
else if indexPath.row == 27 {
cell.messageLabel.text = messages.last
}
return cell
}
Just remove highlighted view from UITableView and it's work like a charm.
Hope it helps.
This is expected behaviour when using coarse cell height estimates (or not providing them at all, as you do). The actual height is computed only when the cells come on screen, so the travel of the scroll bar is adjusted at that time. Expect jumpy insertion/deletion animations too, if you use them.
I hope you heard about this a lot. so take short break and come back on desk and apply 2 - 3 steps for step this.
1) Make sure Autolayouts of label of Cell is setup correct like below.
2) UILabel's number of lines set zero for dynamic height of text.
3) setup automatic dimension height of cell.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
and I believe its should be work. see results of my code.
why you add view in table view , it can also work without it. I just delete that view and change some constraints(like bottom constraints change safe area to superview) , and it works fine.
see this video
download storyboard and add it to your project and then check
Configure your tableview with these in viewDidLoad()
tableView.estimatedRowHeight = 100.0
tableView.rowHeight = UITableView.automaticDimension
tableView.tableFooterView = UIView()
And you should remove both height datasource method.
What you want to do is eliminate the extra blank cells. You can do so by setting the tableFooterView to an empty UIView in the viewDidLoad method. I cloned the code from your GitHub and revised the method:
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "WorldMessageCell", bundle: nil), forCellReuseIdentifier: "WorldMessageCell")
tableView.tableFooterView = UIView()
}
setting the tableFooterView to nil worked for me as well
tableView.tableFooterView = nil

Custom tableview cell image stretched

first post on this website so excuse any faults.
im having trouble with my image in custom cells in tableview, the image is set to width 345, height: 200 and 200 away from border and cellsize is 400 width. But when it launches everything is fine except image is displayed at about 1000 width instead of 200.
the image content mode is set to scale to fill, clips to bounds. but have tried aspect fit, and same problem.
extension SearchViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier:"searchCell", for: indexPath)
return cell
}

Tableview set row height dynamically based on content in swift?

I am struck in setting tableview cell row height in swift , On scrolling the row height is changing, please check the screen shot.
For the first time its scrolling perfect on scrolling multiple times the height is not proper. I am using too many custom tableview cells
In viewDidLoad
I added this
self.tableView.rowHeight = UITableViewAutomaticDimension
and also added these delegate methods.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
return UITableViewAutomaticDimension
}
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
return UITableViewAutomaticDimension
}
Can anyone guide me to achieve this in proper way.

swift tableview image and label

I can't seem to figure how to have a tableview that contains a subject, body and image so that if there is not an image to have the subject and body together. see imageauto lay with table prototype
I would like the subject and body together when there is an image in the database and then if there is an image, then subject image body
pin the labels to the top and bottom of the cell. When you don't have an image you set the imageView.isHidden = true. You also return the regular cell height minus the imageView.frame.size.height in UITableViewDelegate.tableview(_:heightForRowAt:).
If you don't like approach just make two different tableViewCells and deque the one with the image when you have an image and the one without the imageView when you don't have an image.
You can do this in your dataSource methods for the tableview. Something like this should work:
//Model:
var model = [Stuff]
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myprototype", for: indexPath)
cell.subjectLabel.text = model[indexPath.row].subject
cell.bodyLabel.text = model[indexPath.row].body
//assuming the image is an optional
if let image = model[indexPath.row].image {
cell.imageView.image = image
}
return cell
}
You might have to set the autolayout constraints for the imageView to have a low compression resistance.
You might also want to make the cells resize by implementing the following in the UITableViewDelegate methods:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}

Fill UITableViewCell with UITextView (Static Cells)

If I have a static uitableviewcell with a textview inside it, how am I able to set the height of the uitableviewcell to fit all of the textview content on it?
You can use this method.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 50
}
You can use an If-else condition to make it what ever height you want as long as you return a number(CGFloat)