I'm using swift4,I have outlet for table view and table view height:
#IBOutlet var commentsTableView: UITableView!
#IBOutlet var commentTableHeight: NSLayoutConstraint!
the table return dynamic height for each cell
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
I want to change table view height when get data from API:
//// Consum APIs
func getProductDetails() {
API.getProductDetails(category_id: String(self.passed_category_id)) {( error: Error?, success: Bool, products: ProductDetails) in
if success {
self.product = products
self.setProductDetailsValue()
} else {
}
}
}
func setProductDetailsValue() {
if(self.product.comments.count > 0) {
self.commentsTableView.isHidden = false
self.commentsTableView.reloadData()
self.commentsTableView.layoutIfNeeded()
self.commentTableHeight.constant = self.getTableViewHeight(tableView: self.commentsTableView)
print("comment table height: ", self.commentTableHeight.constant, self.getTableViewHeight(tableView: self.commentsTableView))
} else {
self.commentsTableView.isHidden = true
}
}
func getTableViewHeight(tableView: UITableView)-> CGFloat {
tableView.layoutIfNeeded()
return tableView.contentSize.height
}
the print statement prints values like:
comment table height: 352.0 624.333324432373
which mean that the self.commentTableHeight.constant take different value. I find that every time the app give 44 pt to every cell, but I don't know what is the problem with my code.
any help?
You have to re-layout the view after changing the constant
self.commentTableHeight.constant = self.getTableViewHeight(tableView: self.commentsTableView)
self.view.layoutIfNeeded()
print("comment table height: ", self.commentTableHeight.constant, self.getTableViewHeight(tableView: self.commentsTableView))
//
you have to put this in viewDidLoad
tableView.estimatedRowHeight = 200; // your estimated height
tableView.rowHeight = UITableViewAutomaticDimension;
Related
I've a vertical listing with 7 types of UITableViewCells. One of them consist a UITableView inside the cell.
My requirement is the main tableview should autoresize the cell according to the cell's inner tableview contentSize. That os the inner tableview will show its full length and the scrolling will be off.
This approach working fine, but for cell with tableview only. When I introduce a UIImageView (with async loading image) above inner tableview, the total height of cell is somewhat smaller than the actual height of its contents. And so the inner tableview is getting cut from bottom.
Here is a representation of the bug.
I'm setting the height of UImageView according to the width to scale properly:
if let media = communityPost.media, media != "" {
postImageView.sd_setImage(with: URL(string: media), placeholderImage: UIImage(named: "placeholder"), options: .highPriority) { (image, error, cache, url) in
if let image = image {
let newWidth = self.postImageView.frame.width
let scale = newWidth/image.size.width
let newHeight = image.size.height * scale
if newHeight.isFinite && !newHeight.isNaN && newHeight != 0 {
self.postViewHeightConstraint.constant = newHeight
} else {
self.postViewHeightConstraint.constant = 0
}
if let choices = communityPost.choices {
self.datasource = choices
}
self.tableView.reloadData()
}
}
} else {
self.postViewHeightConstraint.constant = 0
if let choices = communityPost.choices {
datasource = choices
}
tableView.reloadData()
}
And the inner table view is a subclass of UITableView :
class PollTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet{
self.invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
}
The main table view is set to resize with automaticDimension :
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? UITableView.automaticDimension
}
Can't seem to understand what is going wrong. Any help is appreciated.
Thanks.
When the table view is asking you to estimate the row height, you are calling back the table view. Thus you are not providing it with any information it doesn't already have.
The problem is probably with your async loading image, so you should predict the image size and provide the table view with properly estimated row height when the image hasn't loaded yet.
I have a question about UITableView.
I want to let the tableView height according to my cells content height.
So, I use the following code successfully.
DispatchQueue.main.async {
var frame = self.tableView.frame
frame.size.height = self.tableView.contentSize.height
self.tableView.frame = frame
}
But when I have much data to show, my contents will out of screen.
And the tableView also out of screen.
Have any ideas to set it's constrain and don't make it out of screen.
I want to set 15 between the tableView bottom layout and superView bottom layout.
I use SnapKit to set autolayout.
//I want to set this one is the biggest frame size. Other contents I can scroll the tableView to show the data.
tableView.snp.makeConstraints { (make) in
make.top.equalTo(self.topLayoutGuide.snp.bottom)
make.left.equalTo(10)
make.right.equalTo(-10)
make.bottom.equalTo(-15)
}
You could create a custom UITableView :
class AutomaticHeightTableView: UITableView {
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height + 20)
}
}
And then set your UITableView Class to AutomaticHeightTableView.
This solution is inspired from an answer found on stackoverflow.
I have solved a similar problem using the following method. You only need few lines of code.
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
private func setupTableView() {
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
Simply set an estimated row height for the UITableView and then set the rowHeight as UITableViewAutomaticDimension.
Maybe you can limit the height of the table's frame, making sure is not longer than its superView, something like this modifying your code:
DispatchQueue.main.async {
if let superViewHeight = self.tableView.superView?.bounds.maxY {
let maxHeight = superViewHeight - self.tableView.frame.minY
var frame = self.tableView.frame
frame.size.height = min(self.tableView.contentSize.height, maxHeight)
self.tableView.frame = frame
}
}
Code Work :
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tblHeightConstraint: NSLayoutConstraint! // tableView Height Constraint
#IBOutlet weak var tblView: UITableView!
var tblMaxHeight : CGFloat = 50
override func viewDidLoad() {
super.viewDidLoad()
let navHeight = (self.navigationController?.navigationBar.frame.size.height)! + UIApplication.shared.statusBarFrame.size.height
tblMaxHeight = self.view.frame.size.height - 40 - navHeight
}
override func viewDidLayoutSubviews(){
tblHeightConstraint.constant = min(tblMaxHeight, tblView.contentSize.height)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 24
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = "Row: #\(indexPath.row)"
return cell!
}
}
Constraints to tableView :
Output :
Put your UITableView inside a UIScrollView and add constraints like this:
Constraints to Table View
Create an IBOutlet of the height constraint and then override viewWillLayoutSubviews in your UIViewController
override func viewWillLayoutSubviews() {
super.updateViewConstraints()
self.tableViewHeightConstraint.constant = tableView.contentSize.height
}
Have you tried
self.tableView.invalidateIntrinsicContentSize()
https://developer.apple.com/documentation/uikit/uiview/1622457-invalidateintrinsiccontentsize
I have a custom subclass of UITableViewController. It has one section containing many rows. Each row corresponds to the same custom table view cell class. Each custom cell has two labels: myLabel1 & myLabel2, both subviews of the cell's contentView.
Every myLabel1 has one line of text, and every myLabel2 has one or two lines of text, but every cell should have the same height, as if every myLabel2 has two lines of text.
The labels use Dynamic Type.
myLabel1.font = UIFont.preferredFont(forTextStyle: .headline)
myLabel2.font = UIFont.preferredFont(forTextStyle: .subheadline)
According to Working with Self-Sizing Table View Cells, I've positioned each label with Auto Layout and "set the table view’s rowHeight property to UITableViewAutomaticDimension" so that the row height changes with Dynamic Type.
How do I make every cell have the same height?
How should I estimate the table view's row height?
UITableViewAutomaticDimension will estimate the cell size depending on cells content size. We must calculate the max height that a cell could possibly have, and return this height for all cells instead of using UITableViewAutomaticDimension.
CustomTableViewCell
let kMargin: CGFloat = 8.0
class CustomTableViewCell: UITableViewCell {
#IBOutlet var singleLineLabel: UILabel!
#IBOutlet var doubleLineLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
updateFonts()
}
override func prepareForReuse() {
updateFonts()
}
func updateFonts() {
singleLineLabel.font = UIFont.preferredFont(forTextStyle:.title3)
doubleLineLabel.font = UIFont.preferredFont(forTextStyle:.body)
}
func updateCellForCellWidth(_ width:CGFloat) {
doubleLineLabel.preferredMaxLayoutWidth = width - (2*kMargin)
}
func fillCellWith(_ firstString: String, _ secondString: String) {
singleLineLabel.text = firstString
doubleLineLabel.text = secondString
}
}
On View Controller
Setting up a dummy cell and listing to notifications for dynamic type
var heightCalculatorDummyCell: CustomTableViewCell!
var maxHeight: CGFloat = 0.0
override func viewDidLoad() {
super.viewDidLoad()
heightCalculatorDummyCell = tableView.dequeueReusableCell(withIdentifier: "cell_id") as! CustomTableViewCell
maxHeight = getMaxHeight()
NotificationCenter.default.addObserver(self, selector: #selector(AutomaticHeightTableViewController.didChangePreferredContentSize), name: .UIContentSizeCategoryDidChange, object:nil)
}
Getting max height using a dummy cell.
func getMaxHeight() -> CGFloat {
heightCalculatorDummyCell.updateFonts()
heightCalculatorDummyCell.fillCellWith("Title","A string that needs more than two lines. A string that needs more than two lines. A string that needs more than two lines. A string that needs more than two lines. A string that needs more than two lines.")
heightCalculatorDummyCell.updateCellForCellWidth(tableView.frame.size.width)
let size = heightCalculatorDummyCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return (size.height + 1)
}
Handling Table view reloads on notification
deinit {
NotificationCenter.default.removeObserver(self)
}
func didChangePreferredContentSize() {
maxHeight = getMaxHeight()
tableView.reloadData()
}
Final step, returning max heights in tableview delegate
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titles.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return maxHeight
}
Make myLabel2 a FixedHeightLabel so that its height is always two lines.
class FixedHeightLabel: TopAlignedLabel {
override var intrinsicContentSize: CGSize {
let oldText = text
text = "\n"
let height = sizeThatFits(CGSize(width: .max, height: .max)).height
text = oldText
return CGSize(width: UIViewNoIntrinsicMetric, height: height)
}
}
class TopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
let textRect = super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
super.drawText(in: textRect)
}
}
I want to implement a method that looks something like this:
setCellHeightForIndexPath(someIndexPath, 80)
and then the table view cell at that index path will suddenly have a height of 80.
The reason I want to do this is because I want the height of the cell to be set to the height of the web view's content after it has finished loading the HTML. Since I can only get the web view's content size after it has finished loading, I can't just set the cell height right away.
See this question for more info.
So in the webViewDidFinishLoad method, I can just get the web view's content height, set the web view's height to that, and call this method to set the cell's height.
It seems like that the cell height can only change when heightForRowAtIndexPath is called. I think the method would use a similar approach as my answer to this question. I think I need to store an array of heights maybe? I just can't think of a way to implement this!
How can I implement such a method?
Note: don't tell me this is not possible. In the Instagram app, I can see different images that have different heights fit perfectly in a table view cell. And those images are similar to my web views. They both need time to load.
EDIT:
Let me show some of my attempts at this:
var results: [Entry] = []
var cellHeights: [CGFloat] = []
var webViews: [UIWebView] = []
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("resultCell")
let webView = cell!.contentView.viewWithTag(1) as! UIWebView
webView.loadHTMLString(results[indexPath.row].htmlDescriptionForSearchMode(.TitleOnly), baseURL: nil)
webView.delegate = self
webView.scrollView.scrollEnabled = false
webViews.append(webView)
cellHeights.append(400)
webView.stringByEvaluatingJavaScriptFromString("highlightSearch(\"day\")")
return cell!
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return indexPath.row < cellHeights.count ? cellHeights[indexPath.row] : 400
}
func webViewDidFinishLoad(webView: UIWebView) {
let height = CGFloat(webView.stringByEvaluatingJavaScriptFromString("document.height")!.toFloat()!)
webView.frame = CGRect(origin: webView.frame.origin, size: CGSizeMake(webView.frame.width, height))
print(height)
if let index = webViews.indexesOf(webView).first {
cellHeights[index] = height
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .None)
tableView.endUpdates()
}
}
results is the stuff that I want to show in the web views. cellHeights is used to store the height of each cell. I put all the web views into the webViews array so I can call indexOf in webViewDidFinishLoad to identify which web view is loaded.
EDIT:
So I wrote this code in my table view controller with reference to Andre's answer:
class SearchResultsController: UITableViewController, UIWebViewDelegate {
var entries: [Entry] = []
lazy var results: [Result] = {
return self.entries.map { Result(entry: $0) }
}()
var cellHeights: [CGFloat] = []
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let result = results[indexPath.section]
var cell = result.cell
if cell == nil {
print("cellForRow called")
cell = tableView.dequeueReusableCellWithIdentifier("resultCell") as! ResultCell
cell.webView.delegate = self
print(cell == nil)
print("loading \(result.entry.title)...")
cell.webView.loadHTMLString(result.entry.htmlDescriptionForSearchMode(.TitleOnly), baseURL: nil)
result.cell = cell
}
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return indexPath.row < cellHeights.count ? cellHeights[indexPath.row] : 400
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 169
tableView.rowHeight = UITableViewAutomaticDimension
tableView.tableFooterView = UIView()
}
func webViewDidFinishLoad(webView: UIWebView) {
print("didFinishLoad called")
if webView.loading {
return
}
guard let cell = webView.superview?.superview as? ResultCell else {
print("could not get cell")
return
}
guard let index = results.map({$0.cell}).indexOf(cell) else {
print("could not get index")
return
}
let result = results[index]
print("finished loading \(result.entry.title)...")
guard let heightString = webView.stringByEvaluatingJavaScriptFromString("document.height") else {
print("could not get heightString")
return
}
guard let contentHeight = Float(heightString) else {
print("could not convert heightString")
return
}
cell.webViewHeightConstraint.constant = CGFloat(contentHeight)
tableView.beginUpdates()
tableView.endUpdates()
}
}
class ResultCell: UITableViewCell {
#IBOutlet weak var webView: UIWebView!
#IBOutlet weak var webViewHeightConstraint: NSLayoutConstraint!
}
class Result {
let entry: Entry
var contentHeight: Float?
var cell: ResultCell!
init(entry: Entry) {
self.entry = entry
}
}
You cannot "push" the new cell height onto a table view. Instead, you need to make table view "pull" the new height from your heightForRowAtIndexPath, and be ready to supply the new height.
When the cell load finishes for row r, you need to update your model in such a way that it knows the new height of row r. After that you need to tell your table view to reload itself, like this:
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
tableView.endUpdates()
This will start the process of updating your cell. heightForRowAtIndexPath will be called. Your code will return the new height. After that cellForRowAtIndexPath will be called. Your code should be prepared to return the cell that has finished loading, without initiating a new data load.
i tried implementing it by using automatic autolayout and automatic cell height calculation.
maybe it helps to point you into the right direction:
https://github.com/andreslotta/WebViewCellHeight
just an excerpt:
func webViewDidFinishLoad(webView: UIWebView) {
if webView.loading {
return
}
guard let cell = webView.superview?.superview as? WebViewCell else {
print("could not get cell")
return
}
guard let index = websites.map({$0.cell}).indexOf(cell) else {
print("could not get index")
return
}
// get website
let website = websites[index]
print("finished loading \(website.urlString)...")
// get contentheight from webview
guard let heightString = webView.stringByEvaluatingJavaScriptFromString("document.height") else {
print("could not get heightString")
return
}
guard let contentHeight = Float(heightString) else {
print("could not convert heightString")
return
}
cell.webViewHeightConstraint.constant = CGFloat(contentHeight)
tableView.beginUpdates()
tableView.endUpdates()
}
You can implement like this
Take one global CGFloat for height and one indexpath
now when you need to change height set both values and use
[self.yourTableview beginUpdate]
[self.yourTableview endUpdate]
will update your cell
and in cellForRowAtIndexPath you should use dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *) will make sure you got updated cell every time
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath == yourIndexpath) {
return gloablVariable;
} else {
return defauleHeight
}
}
Hope it helps
How can I make the TableViewCell change height to make the UILabel fit?
I am not using auto layout in my project, and because this is a big project I am not going to change to that either - so I need a fix that works without auto layout.
This is my CommentsViewController.swift code:
import UIKit
import Parse
import ActiveLabel
class CommentsViewController: UITableViewController, UITextFieldDelegate {
var commentsArray: [String] = []
var currentObjID = ""
#IBOutlet var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
self.textField.delegate = self
queryComments()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func queryComments(){
self.commentsArray.removeAll()
let query = PFQuery(className:"currentUploads")
query.whereKey("objectId", equalTo: self.currentObjID)
query.findObjectsInBackgroundWithBlock { (objects:[PFObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects {
for object in objects {
let list: AnyObject? = object.objectForKey("comments")
self.commentsArray = list! as! NSArray as! [String]
self.tableView.reloadData()
self.textField.text = ""
}
}
} else {
print("\(error?.userInfo)")
}
}
self.sendButton.enabled = true
self.refreshButton.enabled = true
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return commentsArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableViewCell;
if self.commentsArray.count > indexPath.row{
cell.commentsText.font = UIFont.systemFontOfSize(15.0)
cell.commentsText.text = commentsArray[commentsArray.count - 1 - indexPath.row]
cell.commentsText.numberOfLines = 0
}
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
let height:CGFloat = self.calculateHeightForString(commentsArray[indexPath.row])
return height + 70.0
}
func calculateHeightForString(inString:String) -> CGFloat
{
let messageString = inString
let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(15.0)]
let attrString:NSAttributedString? = NSAttributedString(string: messageString, attributes: attributes)
let rect:CGRect = attrString!.boundingRectWithSize(CGSizeMake(300.0,CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, context:nil )//hear u will get nearer height not the exact value
let requredSize:CGRect = rect
return requredSize.height //to include button's in your tableview
}
}
Screenshot:
This makes all the cells very big, even the cells that only has 1 line. Any ideas?
Im not 100% sure without Autolayout but you could set the estimated row height along with dimension. So in your viewDidLoad enter this
self.tableView.estimatedRowHeight = //Largest Cell Height
self.tableView.rowHeight = UITableViewAutomaticDimension
Can you tell what happens when you remove the heightForRowAtIndexPath method and add this in your viewDidLoad:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 80
With these two lines we instruct the tableview to calculate the cell's size matching its content and render it dynamically.
EDIT: I just read you don't want to use Auto Layout. I don't think if this'll still work in that case.
You can use heightForRowAtIndexPath to edit a table cell's height. This is a delegate method you'll be able to use after subclassing and setting your tableview's delegate property (IBoutlet or view.delegate = self)
See this thread if you don't already know your label's height: how to give dynamic height to UIlabel programatically in swift?
The way this works is you'll give a height for every index path row (ideally out of some collection - array). As your table loads cells it will automatically adjust for you.