I have a very similar question as posted here (Dynamic UIImageView Size Within UITableView) where I'm trying to dynamically retrieve an image from Firebase and make the resulting tableview adjust to the height of the image given a fixed width across the screen based on the aspect ratio. All the articles I read says to make the cell calculation based on cellforRowAt, but my actual image is within the TableViewCell. Can someone please help?
Tableview controller:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedTableViewCell", for: indexPath) as! FeedTableViewCell
cell.configureCell(post: postArray[indexPath.row])
return cell
}
TableViewCell:
func configureCell(post: Post){
self.postImage.sd_setImage(with: URL(string: post.postImageURL),
placeholderImage: UIImage(named: "default"))
}
you have cell.setNeedsLayout() inside the completion handler of setting image method, like this:
cell. postImage.kf.setImage(with: URL) { _, _, _, _ in
cell.layoutIfNeeded()
}
First of all, you need to use Autolayout to calculate the proper cell height by creating a proper set of constraints that would determine the cell height based on content. I am going to assume you did that.
Then you have to tell the tableView you want it to use Autolayout to calculate height:
// my best estimation of the height
tableView.estimatedRowHeight = 144
// but the real height is calculated by autolayout
tableView.rowHeight = UITableViewAutomaticDimension
Now this would work if the autolayout could calculate the height correctly in cellForRowAt. But since the image is downloaded asynchronously, the image is set later, when the cell may be already presented. This requires you to provide a way in which a cell can tell the tableView that it has downloaded its content and its layout needs to be recalculated. To do so, use this method in the viewController with the tableView:
func recalculateTableViewLayout() {
self.tableView.beginUpdates()
self.tableView.setNeedsLayout()
self.tableView.endUpdates()
}
You will need to pass the reference to the viewController with the tableView to each cell (I recommend to use delegate pattern for that, here for brevity I will simply sketch it using it directly):
class FeedTableViewCell: UITableViewCell {
weak var feedViewController: FeedViewController?
// etc.
And in the cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedTableViewCell", for: indexPath) as! FeedTableViewCell
cell.feedViewController = self
cell.configureCell(post: postArray[indexPath.row])
return cell
}
Then use completion handler of sd_setImage to tell the tableView to recalculate its layout when the image gets downloaded:
func configureCell(post: Post){
self.postImage.sd_setImage(with: URL(string: post.postImageURL), placeholderImage: UIImage(named: "default")) { (image, error, cache, url) in
self.feedViewController?.recalculateTableViewLayout()
}
}
Related
I have a tableView and cells. The Cells are loaded from a xib and they have a label with automatic height. I need to narrow one cell if the user taps on it.
I have tried hiding - doesn't work
I have tried removeFromSuperView()- doesn't work
Is there any alternative?
When setting up your tableViewCell store the height anchor you want to update
var yourLabelHeightAnchor: NSLayoutConstraint?
private func setupLayout() {
yourLabelHeightAnchor = yourLabel.heightAnchor.constraint(equalToConstant: 50)
// Deactivate your height anchor as you want first the content to determine the height
yourLabelHeightAnchor?.isActive = false
}
When the user clicks on a cell, notify the tableView that the cell is going to change, and activate the height anchor of your cell.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourTableViewCellIdentifier") as? YourCell
self.tableView.beginUpdates()
cell?.yourLabelHeightAnchor?.isActive = true
self.tableView.endUpdates()
}
Did you try to do something like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var result: CGFloat
if (indexPath.row==0) {
result = 50 }
else {result = 130}
return result
}
This is just an example where height is changed for the first row. I tested on my application and it gave result like this.
I have a table view. First, I load text data for all cells from database. Next, I load image for every cell from another database. But when I tried to set images, they are not displayed.
If I use
let cell = myTableView.dataSource?.tableView(myTableView,
cellForRowAt: indexPath) as? MyTableViewCell
then images loaded only in invisible cells. It means that those cells that I see the very first (before scrolling down) don't show their images. But all others do.
Another way, if I use
let cell = myTableView.cellForRow(at: indexPath) as? MyTableViewCell
then the opposite happens - only those cells that I see show their images, but all others don't.
After that I do
cell.setImage(image: image)
func setImage(image: UIImage) {
guard let myImageView = myImageView else { return }
myImageView.image = image
}
I set images not in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {...} because as I said, images come after creating cells.
You need to refresh the Table View in the main thread after the load. You can do it with:
DispatchQueue.main.async {
self.yourTableView.reloadData()
}
How to update tableview cell height after updating image height constraint of image downloaded async?
How to trigger tableView cell relayout after image downloaded and constraints changed?
What's the best method to do this?
Already tried putting the code inside Dispatch main queue, but same bad results. I'm doing this in cellForRow method, also moved it to willDisplayCell. Again and again this problem...
Example of code using Kingfisher library for image caching:
if let imgLink = post.imageLink {
if let url = URL(string: imgLink) {
cell.postImage.kf.setImage(with: url, placeholder: UIImage(), options: nil, progressBlock: nil) { (image, error, cacheType, imageURL) in
if let image = image, cell.tag == indexPath.row {
cell.heightConstraint.constant = image.size.height * cell.frame.size.width / image.size.width
}
}
}
}
You may try to call these 2 lines to cause cell heights be recalculated after an image becomes available:
tableView.beginUpdates()
tableView.endUpdates()
See in the documentation https://developer.apple.com/documentation/uikit/uitableview/1614908-beginupdates: "You can also use this method followed by the endUpdates() method to animate the change in the row heights without reloading the cell."
IMHO, As ppalancica pointed out calling beginUpdates and endUpdates is the ideal way. You can't refer tableView from inside UITableViewCell and the proper way is to use a delegate and call beginUpdates and endUpdates from ViewController implementing delegate.
Delegate:
protocol ImageCellDelegate {
func onLayoutChangeNeeded()
}
UITableViewCell implementation:
class ImageCell: UITableViewCell {
var imageView: UIImageView = ...
var delegate: ImageCellDelegate?
...
func setImage(image: UIImage) {
imageView.image = image
//calling delegate implemented in 'ViewController'
delegate?.onLayoutChangeNeeded()
}
...
}
ViewController Implementation:
class ViewController: UIViewController, UITableViewDataSource, ImageCellDelegate {
var tableView: UITableView = ...
.....
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let imageCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! ImageCell
//setting 'delegate' here
imageCell.delegate = self
return imageCell
}
//called from 'ImageCell' when 'image' is set inside 'setImage'
func onLayoutChangeNeeded() {
tableView.beginUpdates()
tableView.endUpdates()
}
.....
}
I had the same problem. What you need to remember is Tableview reuse the cell and you are loading image async.
Recommended: You can do is to request your backhand team to provide you height and width of image so you can calculate cell height and return asap.
If you can't do that you can keep size of dowloaded image in your datasource. so before you download image check your datasource for size of image and update height constraint constant.
Another thing is you should do it in both cellForRow and willDisplay cell (I know it is not good practice but to satisfy tableview automatic dimension)
after update height constant you should use this pair of code to reload your cell.
cell.setNeedsLayout()
cell.layoutIfNeeded()
How I did
if let imagesize = post.imageSize {
cell.updateHeightPerRatio(with: imagesize)
} else {
// Here load size from URL and update your datasource
}
// Load image from URL with any SDWebimage or any other library you used
What I actually did and worked somehow is the following:
if (self.firstLoaded[indexPath.row] == false) {
self.firstLoaded[indexPath.row] = true
DispatchQueue.main.async {
self.tableViewController.tableView.reloadData()
}
}
firstLoaded just tells the table that this row has already received image from URL and calculated / stored correct height.
Also, I used this:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeights[indexPath] {
return height
}
return 1500
}
I know that calling reloadData() is not a good practice, but it solved my problem. If anybody has some other advices, please do not hesitate to share it.
Thanks!
I am using dynamic height for tableview cells and Haneke for downloading and caching image using the following code:
override func viewDidLoad() {
super.viewDidLoad()
myTableView.rowHeight = UITableViewAutomaticDimension
myTableView.estimatedRowHeight = 500
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:ImageCellTableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ImageCellTableViewCell;
cell.myImageView.hnk_setImage(from: URL(string: imageUrl), placeholder: nil)
return cell
}
Here I need to resize the cell height when setting image after downloading.
How can I resize the tableview cell after downloading the image.
Just call
tableView.beginUpdates()
tableView.endUpdates()
after image is downloaded and height constraint is set. Hope this helps!
EDIT:
Another option, to give the right height to the cell, would be receiving the image size from the server. This way you could feed table with just right calculated cell height.
just add the proper constraint to your image, for example , top and left constraint with, aspect ratio constraint.(you can do it even in your storyboard interface)
Set table view's row height like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:ImageCellTableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ImageCellTableViewCell;
cell.myImageView.hnk_setImage(from: URL(string: imageUrl), placeholder: nil)
tableView.rowHeight = cell.myImageView.frame.origin.y + cell.myImageView.frame.size.height + (constant what every you want to give like: - 20/30 as per your need)
return cell
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "approve_cell");
let event = self.events[indexPath.row];
cell.textLabel?.text = event.name;
return cell;
}
I placed 2 images and a label in tableviewcell. When i run the app, images are not visible. What could be the reason?
Check that you are not using the basic style of the UITableViewCell because if you are then that style only includes labels.. no image views. Change it to subtitle or one of the details.
For reference, see: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/TableView_iPhone/TableViewCells/TableViewCells.html
You probably forgot to set AutoLayoutConstraints of your image view. You could either add constraints to that image view through storyboard or through code. The other method is setting the frame of your image view in -(void)viewDidLayoutSubviews, because when you're using auto-layout, the layout of those views will be undefined if its constraints doesn't fulfill system's computing need. So you should either set right constraints or give a specific frame to those undefined views after all of the others has-constraints-views are arranged.
You creat cell in storyboard but you never use this cell created from storyboard. instead you manually creat it by UITableViewCell's designated initializer without override layout method of it.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "approve_cell", for: indexPath)
let event = self.events[indexPath.row];
cell.textLabel?.text = event.name;
return cell;
}
You need to set either Autolayout or used Autoresizing. For autolayout just pinned the proper leading, trailing, top and bottom constraint.