I would like to set the UITableView to match the height for all the contents in the table view.
This is my storyboard
The problem with this is the top and bottom ImageView is always static on the screen.
The there are suppose to be 10 items on the table view but only 7 shows up due to screen size limitation. I would like to show all 10 before user is able to see the bottom ImageView. (btw, all 3 of the views ie. both the image views and tableview is in a uiscrollview)
IDEAL
Some of the other limitations that i have to work with is that the number of items in the table view is dynamic meaning it can be in any amount of usually less than 10 that i will later retrieve from an api. And the cell height is also dynamic depending on the contents.
I have only just started with some simple code
class ExampleViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var items: [String] = [
"Item 01", "Item 02", "Item 03", "Item 04", "Item 05",
"Item 06", "Item 07", "Item 08", "Item 09", "Item 10"]
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
}
Subclass your UITableView to override the intrinsicContentSize to be its contentSize, like this:
override var intrinsicContentSize: CGSize {
return contentSize
}
Then use automatic row heights for your table, so your exampleViewController's viewDidLoad would have:
tableView.estimatedRowHeight = 44
And the UITableViewDelegate function:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
When you receive data from your API and reload your table, just call:
tableView.invalidateIntrinsicContentSize()
This will tell your table to resize itself to the same size as its contents (because of the override), and move your bottom image as needed.
If your storyboard throws an error saying that your UIScrollView has an ambiguous height because there's no height constraint on the UITableView, select your UITableView and give it a placeholder intrinsic size in the Size Inspector.
The answers using the subclassing technique are incomplete. You should also override layoutSubviews() like this.
public class DynamicSizeTableView: UITableView
{
override public func layoutSubviews() {
super.layoutSubviews()
if bounds.size != intrinsicContentSize {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
return contentSize
}
}
This is what I utilize in production apps:
Swift 5, 2021
import UIKit
class DynamicTableView: UITableView {
/// Will assign automatic dimension to the rowHeight variable
/// Will asign the value of this variable to estimated row height.
var dynamicRowHeight: CGFloat = UITableView.automaticDimension {
didSet {
rowHeight = UITableView.automaticDimension
estimatedRowHeight = dynamicRowHeight
}
}
public override var intrinsicContentSize: CGSize { contentSize }
public override func layoutSubviews() {
super.layoutSubviews()
if !bounds.size.equalTo(intrinsicContentSize) {
invalidateIntrinsicContentSize()
}
}
}
You need to set an IBOutlet to the NSLayoutConstraint that sets the tableView height (first you need create the height constraint with any value, doesn't matter) and then ctrl drag it to your class file
Then in your viewWillAppear you have to calculate the tableView height and set it. Like this:
var tableViewHeight:CGFloat = 0;
for (var i = tableView(self.tableView , numberOfRowsInSection: 0) - 1; i>0; i-=1 ){
tableViewHeight = height + tableView(self.tableView, heightForRowAtIndexPath: NSIndexPath(forRow: i, inSection: 0) )
}
tableViewHeightLayout.constant = tableViewHeight
And that's pretty much it. That will give your scrollView content size and shouldn't raise any warnings.
Update Swift 4
this code working be good
self.scrollView.layoutIfNeeded()
self.view.layoutIfNeeded()
self.tableViewHeightConstraint.constant = CGFloat(self.tableView.contentSize.height)
You probably have to implement the table view intrinsic content size. Please check this answer to see if it helps.
I remember having this problem and even created a custom UITableView subclass.
#import "IntrinsicTableView.h"
#implementation IntrinsicTableView
#pragma mark - UIView
- (CGSize)intrinsicContentSize
{
return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}
#pragma mark - UITableView
- (void)endUpdates
{
[super endUpdates];
[self invalidateIntrinsicContentSize];
}
- (void)reloadData
{
[super reloadData];
[self invalidateIntrinsicContentSize];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
#end
Update for Swift 5. Adding maxHeight so that you can specify how tall you want your tableView to be
class SelfSizingTableView: UITableView {
var maxHeight = CGFloat.infinity
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let height = min(maxHeight, contentSize.height)
return CGSize(width: contentSize.width, height: height)
}
}
In that case, don't make your bottom cell static, make it a part of table view and insert this bottom image in last row using table view delegate method - insertRowAtIndexPath
In this type of case add your bottom imageView(red) in a table footer view.
To add footer view in UITableView you can use:
tableViewObj.tableFooterView = footerViewObj;
Try this also
in ViewDidLoad
self.table.estimatedRowHeight = 44.0 ;
self.table.rowHeight = UITableViewAutomaticDimension;
Height for row at index path
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;}
Easy way here.
Step 1: Set a height constraint for the table view
Step 2: Control drag the constraint
Step 3: Before you return the count of the rows. In numberOfRowsInSection method, do
tableViewHeightConstraint.constant = tableView.rowHeight * CGFloat(someArray.count)
Of course you can edit the height anchor programmatically, the logic here is to adjust the table view height according to the cell height and cell number.
Based on solution #nikans, written in Xamarin
[Register(nameof(DynamicSizeTableView)), DesignTimeVisible(true)]
public class DynamicSizeTableView : UITableView
{
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Bounds.Size != IntrinsicContentSize)
InvalidateIntrinsicContentSize();
}
public override CGSize IntrinsicContentSize => ContentSize;
public DynamicSizeTableView(CGRect frame) : base(frame) { }
public DynamicSizeTableView(IntPtr handle) : base(handle) { }
}
Here is the simplest Solution
First Give a height to the tableView.
Create outlet of that height in view Controller. let's say tableViewHeight
Then do this in viewDidLoad or where you populate the data after calling tableView.reloadData()
var height = 0.0
for i in 0..<items.count {
let frame = tableView.rectForRow(at: IndexPath(row: i, section: 0))
height += frame.size.height
}
tableViewHeight.constant = height
This Also Works with tableViews that have dynamic cell heights
Based on solution of #rr1g0
Updated for Swift 5 in 2020, and works with TableViews with sections too.
Create height constraint for tableView and create an outlet to it. And in viewDidLayoutSubviews() use the code below:
var tableViewHeight: CGFloat = 0
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberOfRows(inSection: section) {
tableViewHeight += tableView(tableView, heightForRowAt: IndexPath(row: row, section: section))
}
}
tableViewHeightConstraint.constant = tableViewHeight
Related
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 dragged a UIView to header of UITableView of UITableViewController , this view works pretty well as a header to table, but has abnormal height , its all done inside a storyboard, I want to know how I can control its height. Whatever height I set in Storyboard it doesn't affect , it always shows same height. See the attached image the gray part is UIView as a header.
There are two types of headers in UITableView
UITableView header
UITableView Section headers
Assuming you are talking about the first case.
Static Header View Height
If your header has static height and it is placed in the sotrybaord inside the Table View, you can provide height in the size inspector.
Dynamic Header View Height
If your header height is dynamic you need to go for programmatic approach.
Create a header, with all subview with their constraints, connect it to you headerView property in the Table View Controller.
Create a method updateSizeForHeaderView that will update the header view height based on the tableview width and the content inside the header.
Call you updateSizeForHeaderView from viewWillLayoutSubviews so that on rotation of screen our header view is updated.
In viewDidLoad set you header tableView.tableHeaderView = headerView
Thats all.
class TableViewController: UITableViewController {
#IBOutlet var headerView : UIView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableHeaderView = headerView
}
override func viewWillLayoutSubviews() {
updateSizeForHeaderView(inTableView: tableView)
}
func updateSizeForHeaderView(inTableView tableView : UITableView) {
let size = headerView.systemLayoutSizeFittingSize(tableView.frame.size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
headerView.frame.size = size
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 5
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell_ID", forIndexPath: indexPath)
// Configure the cell...
return cell
}
}
Expanding on #BangOperator answer, you need to reassign the header on the table view afterward, or you may encounter strange behavior... this forces the table view to a proper update
...
#property (nonatomic, assign) CGSize lastTableViewHeaderSize;
#property (nonatomic, assign) BOOL tableViewHeaderSizeChanged;
#end
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
CGSize size = [self.tableView.tableHeaderView systemLayoutSizeFittingSize:self.tableView.bounds.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityDefaultLow];
if (!CGSizeEqualToSize(size, self.lastTableViewHeaderSize)) {
CGRect frame = self.tableView.tableHeaderView.frame;
frame.size = size;
self.tableView.tableHeaderView.frame = frame;
self.tableViewHeaderSizeChanged = YES;
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (self.tableViewHeaderSizeChanged) {
self.tableViewHeaderSizeChanged = NO;
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}
}
This might work:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 30;
}
Please do following might help to you.
Via programmatically
self.automaticallyAdjustsScrollViewInsets = NO;
EDITED
self.tableView.contentInset = UIEdgeInsetsMake(-36, 0, 0, 0);
In my requirement currently,I wanted to develop a Pintrest layout view. For that I have 2 tableView's on same ScrollView.
mark: I didn't use collectionView as it was very difficult to customize the flow Layout for this purpose & I do not want to include a 3rd Party framework for the same.
Check the attached screenshot -
I am populating them with 2 arrays one for even & one for odd items.
I am making these tableView's non scrollable & increasing my scrollView's contentView's height as per the tallest tableView. Both the tableView's have custom cells with dynamically increasing contents i.e a label.
in my viewDidLoad()
self.tableViewCol1.estimatedRowHeight = 296
self.tableViewCol1.rowHeight = UITableViewAutomaticDimension
self.tableViewCol1.separatorStyle = UITableViewCellSeparatorStyle.None
self.tableViewCol2.estimatedRowHeight = 296
self.tableViewCol2.rowHeight = UITableViewAutomaticDimension
self.tableViewCol2.separatorStyle = UITableViewCellSeparatorStyle.None
in my DataSource method -
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell2 = MostLikedTVCscnd()//custom Cell for 2nd Table
var cell1 = MostLikedTVC()//custom Cell for 1st Table
if tableView == tableViewCol1
{
let cell = tableViewCol1.dequeueReusableCellWithIdentifier("cell1", forIndexPath: indexPath) as! MostLikedTVC
cell.imageCol1.image = imageArr1[indexPath.row] as? UIImage
cell.aboutLblCol1.text = labelArr1[indexPath.row] as? String//dynamic increasing label
cell1=cell
}
else if tableView == tableViewCol2
{
let cell = tableViewCol2.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath) as! MostLikedTVCscnd
cell.imageCol2.image = imageArr2[indexPath.row] as? UIImage
cell.aboutLblCol2.text = labelArr2[indexPath.row] as? String
cell2 = cell
}
//changing the height constraint of table's to make the table view non scrollable
tableView1HghtCnstrnt.constant = tableViewCol1.contentSize.height
tableView2HghtCnstrnt.constant = tableViewCol2.contentSize.height
//comparing the content size of table's to check which table is the tallest & adjust the height of the main ScrollView's content
if tableViewCol1.contentSize.height>tableViewCol2.contentSize.height
{
mainViewHghtCnstrnt.constant = tableViewCol1.contentSize.height+35//mainViewHghtCnstrnt :- mainScrollView's content height constraint & 35 is the padding
}
else if tableViewCol1.contentSize.height<tableViewCol2.contentSize.height
{
mainViewHghtCnstrnt.constant = tableViewCol2.contentSize.height+35
}
else if tableViewCol1.contentSize.height==tableViewCol2.contentSize.height
{
mainViewHghtCnstrnt.constant = tableViewCol2.contentSize.height+35
}
//returning the cell
if tableView == tableViewCol1
{
return cell1
}
else
{
return cell2
}
}
}
But my problem is that the table's are not properly calculating the size of their content's. I did some search and here an answer to a question says - contentSize will be messed up when you give estimatedRowHeight
So what options do I have? What can be done to realise the same properly?
You can use tableView's contentSize property to get the height required for the tableView.
I have done a demo to test this, and it is working as you are expecting.
My constraint are as follows:
ScrollView:
LeadingSpace to SuperView, TrailingSpace to SuperView, TopSpace to
SuperView, BottomSpace to SuperView
ContainerView (UIView inside scrollView):
LeadingSpace to SuperView, TrailingSpace to SuperView, TopSpace to
SuperView, BottomSpace to SuperView, EqualWidth to SuperView (i.e
scrollView)
ATableView
LeadingSpace to SuperView, TrailingSpace to BTableView, TopSpace to
SuperView, Width fixed points, HeightFixed points (Created outlet of
this constraint), BottomSpace to SuperView with StandardSpacing and
LowPriority (250)
BTableView
LeadingSpace to ATableView, TrailingSpace to SuperView, TopSpace to
SuperView, Width fixed points, HeightFixed points (Created outlet of
this constraint), BottomSpace to SuperView with StandardSpacing and
LowPriority (250)
And here is my full code,
class ViewController: UIViewController {
#IBOutlet weak var tableViewA: UITableView!
#IBOutlet weak var tableViewB: UITableView!
#IBOutlet weak var heightConstraintBTableView: NSLayoutConstraint!
#IBOutlet weak var heightConstraintATableView: NSLayoutConstraint!
var tableViewACellCount = 50
var tableViewBCellCound = 20
override func viewDidLoad() {
super.viewDidLoad()
//Dont set estimatedRowHeight as it is displaying empty cell at the bottom of tableView (try playing with this uncomment estimatedRowHeight)
//tableViewA.estimatedRowHeight = 44.0
tableViewA.rowHeight = UITableViewAutomaticDimension
//tableViewB.estimatedRowHeight = 44.0
tableViewB.rowHeight = UITableViewAutomaticDimension
//Try both methods
self.performSelector("adjustTableViewHeight", withObject: [], afterDelay: 0.0)
//self.adjustTableViewHeight()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if(tableView.isEqual(tableViewA)) { //A TableView
return tableViewACellCount
} else { //B TableView
return tableViewBCellCound
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("cell")
return cell!
}
func adjustTableViewHeight() {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.tableViewA.layoutIfNeeded()
self.tableViewB.layoutIfNeeded()
}
heightConstraintATableView.constant = tableViewA.contentSize.height
heightConstraintBTableView.constant = tableViewB.contentSize.height
}
}
The basic setup is like this: I have a UITableView with two proto cells that I am working with (think of a messaging app, where one cell type shows messages you send and the other, messages you recieve). Now obviously the message length can vary from one line to even 100+ lines thus I need variable cell heights.
First attempt:
tableView.estimatedRowHeight = 75
tableView.rowHeight = UITableViewAutomaticDimension
I used estimatedRowHeight in my viewDidLoad(). This works perfectly and computes cell heights very nicely. But because this is a messaging app I need to scroll the tableView all the way to bottom on viewDidLoad() and whenever a new message is recieved / sent. But the estimatedRowHeight messes with the tableView scrolling all the way to bottom. Some say it's a bug, some say it's to be expected. Nonetheless, this way won't work for me at all.
Second Attempt:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
I thought to compute heights manually as so:
let measuerWidth = UIScreen.mainScreen().bounds
var w = measuerWidth.size.width // this is so that we can limit label width to screen width so the text is forced to go to multiple lines
// I probably should use my custom cell width here, but if I try to `dequeue` that cell it's frame contents are always `zero`. Is that a problem?
let lbl = UILabel(frame: CGRect.zeroRect)
lbl.text = chatMessages[indexPath.row][ChatRoomKeys.MESSAGE_TEXT] as? String
lbl.font = UIFont.systemFontOfSize(20)
lbl.numberOfLines = 0
lbl.lineBreakMode = NSLineBreakMode.ByWordWrapping
lbl.frame = CGRectMake(0, 0, w, 0)
lbl.sizeToFit()
return lbl.frame.height + 20
This way works almost perfectly however at times cell height isn't what it should be. Meaning sometimes if the text is one line plus one word, that one word won't show because the cell height was only for one line.
Is there some better way to calculate the cell height?
UPDATE:
Here's a screenshot of kinda what happens
as you can see the label ends at x but the original text goes upto y.
UPDATE 2:
These are the proto cells I am using, the bottom cell is simply a mirror of the top one.
I use this one:
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 2)
{
NSString *cellText = #"init some text";
CGSize labelSize = [self calculateTextSize:cellText];
return labelSize.height + 20.0f;
}
return 45;
}
- (CGSize) calculateTextSize: (NSString*) text
{
UIFont *cellFont = [UIFont fontWithName:#"HelveticaNeue" size:12.0];
CGFloat width = CGRectGetWidth(self.tableView.frame) - 40.0f;
CGSize constraintSize = CGSizeMake(width, MAXFLOAT);
CGRect labelRect = [cellText boundingRectWithSize:constraintSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName:cellFont} context:NSLineBreakByWordWrapping];
CGSize labelSize = labelRect.size;
return labelSize;
}
Edit:
CGFloat width = CGRectGetWidth(self.tableView.frame) - 40.0f;
Calculation of the maximum available width of the text, you can use self.view.frame or etc. -40.0f - because i have indentation from the edge = 20.0f
You can do this with the automatic height as you were doing. See the code below, when your view is about to appear, ask the UITableView for the number of rows in the section (0) in this case. Then you can create an NSIndexPath for the last row in that section then ask the UITableView to scroll that index path into view. Using the code below you can still have the UITableView calculate the heights for you.
import UIKit
final class ViewController: UIViewController {
// MARK: - Constants
let kFromCellIdentifier = "FromCell"
let kToCellIdentifier = "ToCell"
// MARK: - IBOutlets
#IBOutlet private weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 75.0
tableView.rowHeight = UITableViewAutomaticDimension
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let numRows = tableView.numberOfRowsInSection(0) - 1 // -1 because numbering in the array starts from 0 not 1
let indexPath = NSIndexPath(forRow: numRows, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
}
}
extension ViewController: UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row % 2 == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier(kFromCellIdentifier, forIndexPath: indexPath) as! CustomTableViewCell
cell.configureCell(UIImage(named: "Talk")!, text: "From Some text \(indexPath.row)")
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier(kToCellIdentifier, forIndexPath: indexPath) as! CustomTableViewCell
cell.configureCell(UIImage(named: "Email")!, text: "To Some text \(indexPath.row)")
return cell
}
}
}
When you insert your first UITableViewCell with insertRowsAtIndexPaths:withRowAnimation:, it usually appears at the top of the UITableView. In the Periscope app, the opposite happens - the first inserted cell is bottom aligned. As new cells are pushed in, the old cells move up in the table. How is this achieved?
In case you're interested in how I did it in the Periscope iOS app, it's actually pretty simple...
TL;DR; Add a transparent table header header view with a height equal to your table view frame's height. Then, as you add cells to your table, simply animate the table view's content offset.
Give your table view a header view:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *headerView = [[UIView alloc] initWithFrame:CGRectZero];
headerView.userInteractionEnabled = NO; // all touches within this space must go through to the video layer
return headerView; // empty header above chat, so messages flow in from bottom
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return self.tableView.frame.size.height; // empty header above chat, so messages flow in from bottom
}
Add data to your table (in my case, messages get added to an array called _messages. I then call reloadData on the UITableViewController). Then call this method to animate the cells in:
- (void)scrollTableToBottom
{
if (!self.isViewLoaded || _messages.count == 0)
return;
CGFloat offsetY = self.tableView.contentSize.height - self.tableView.frame.size.height + self.tableView.contentInset.bottom;
[UIView animateWithDuration:0.33
delay:0
options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
}
completion:nil];
}
Hope that helps. I found this to be a pretty cheap/simple way of simulating cells anchored to the bottom. I know some people have mentioned flipping the table upside down, but that just seems crazy to me. :-)
Swift 4 version of Aaron Wasserman answer.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: .zero)
headerView.isUserInteractionEnabled = false
return headerView
}
// Added logic to avoid blank space at the top
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.frame.size.height - CGFloat(messages.count * cellHeight)
}
func scrollToBottom(animated: Bool) {
if isViewLoaded && messages.count > 0 {
let offset = tableView.contentSize.height - tableView.frame.size.height + tableView.contentInset.bottom
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut, .allowUserInteraction], animations: {
self.tableView.setContentOffset(CGPoint(x: 0, y: offset), animated: animated)
}, completion: nil)
}
}
scrollToBottom method needs to be called after tableView.reloadData()
My modification of the mr.Wasserman answer
extension MyViewController : UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: .zero)
headerView.isUserInteractionEnabled = false
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let diff = tableView.contentSize.height - tableView.bounds.height
return diff > 0 ? 0 : -diff
}
}
You can use the insertRowsAtIndexPath method, and then get the tableview to scroll to bottom.
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:self.posts.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
If you wish to have that giant gap at the top of your tableview you can set the scroll offset, and adjust this as new items come in.
Another way, would be to flip the tableview upside down, and then flip each row upside down.
Bit late to the party, but here's another approach.
It can be done without having to add arbitrary header views. Adjust the content offset whenever the content size changes.
class BottomEndianTableView: UITableView {
private var observer: Any?
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
observer = observe(\.contentSize) { _, _ in
DispatchQueue.main.async { [weak self] in
self?.scrollToEnd(animated: false)
}
}
}
func scrollToEnd(animated: Bool) {
let scrollDistance = contentSize.height - frame.height
setContentOffset(CGPoint(x: 0, y: scrollDistance), animated: animated)
}
}
You can also call scrollToEnd(animated: true) in response to the keyboard displaying