UITableViewCell with UITextView height in iOS 7?

90,750

Solution 1

First of all, it is very important to note, that there is a big difference between UITextView and UILabel when it comes to how text is rendered. Not only does UITextView have insets on all borders, but also the text layout inside it is slightly different.

Therefore, sizeWithFont: is a bad way to go for UITextViews.

Instead UITextView itself has a function called sizeThatFits: which will return the smallest size needed to display all contents of the UITextView inside a bounding box, that you can specify.

The following will work equally for both iOS 7 and older versions and as of right now does not include any methods, that are deprecated.


Simple Solution

- (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width {
    UITextView *calculationView = [[UITextView alloc] init];
    [calculationView setAttributedText:text];
    CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
    return size.height;
}

This function will take a NSAttributedString and the desired width as a CGFloat and return the height needed


Detailed Solution

Since I have recently done something similar, I thought I would also share some solutions to the connected Issues I encountered. I hope it will help somebody.

This is far more in depth and will cover the following:

  • Of course: setting the height of a UITableViewCell based on the size needed to display the full contents of a contained UITextView
  • Respond to text changes (and animate the height changes of the row)
  • Keeping the cursor inside the visible area and keeping first responder on the UITextView when resizing the UITableViewCell while editing

If you are working with a static table view or you only have a known number of UITextViews, you can potentially make step 2 much simpler.

1. First, overwrite the heightForRowAtIndexPath:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // check here, if it is one of the cells, that needs to be resized
    // to the size of the contained UITextView
    if (  )             
        return [self textViewHeightForRowAtIndexPath:indexPath];
    else
    // return your normal height here:
            return 100.0;           
}

2. Define the function that calculated the needed height:

Add an NSMutableDictionary (in this example called textViews) as an instance variable to your UITableViewController subclass.

Use this dictionary to store references to the individual UITextViews like so:

(and yes, indexPaths are valid keys for dictionaries)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Do you cell configuring ...

    [textViews setObject:cell.textView forKey:indexPath];
    [cell.textView setDelegate: self]; // Needed for step 3

    return cell;
}

This function will now calculate the actual height:

- (CGFloat)textViewHeightForRowAtIndexPath: (NSIndexPath*)indexPath {
    UITextView *calculationView = [textViews objectForKey: indexPath];
    CGFloat textViewWidth = calculationView.frame.size.width;
    if (!calculationView.attributedText) {
        // This will be needed on load, when the text view is not inited yet
        
        calculationView = [[UITextView alloc] init];
        calculationView.attributedText = // get the text from your datasource add attributes and insert here
        textViewWidth = 290.0; // Insert the width of your UITextViews or include calculations to set it accordingly
    }
    CGSize size = [calculationView sizeThatFits:CGSizeMake(textViewWidth, FLT_MAX)];
    return size.height;
}

3. Enable Resizing while Editing

For the next two functions, it is important, that the delegate of the UITextViews is set to your UITableViewController. If you need something else as the delegate, you can work around it by making the relevant calls from there or using the appropriate NSNotificationCenter hooks.

- (void)textViewDidChange:(UITextView *)textView {

    [self.tableView beginUpdates]; // This will cause an animated update of
    [self.tableView endUpdates];   // the height of your UITableViewCell

    // If the UITextView is not automatically resized (e.g. through autolayout 
    // constraints), resize it here

    [self scrollToCursorForTextView:textView]; // OPTIONAL: Follow cursor
}

4. Follow cursor while Editing

- (void)textViewDidBeginEditing:(UITextView *)textView {
    [self scrollToCursorForTextView:textView];
}

This will make the UITableView scroll to the position of the cursor, if it is not inside the visible Rect of the UITableView:

- (void)scrollToCursorForTextView: (UITextView*)textView {
    
    CGRect cursorRect = [textView caretRectForPosition:textView.selectedTextRange.start];
    
    cursorRect = [self.tableView convertRect:cursorRect fromView:textView];
    
    if (![self rectVisible:cursorRect]) {
        cursorRect.size.height += 8; // To add some space underneath the cursor
        [self.tableView scrollRectToVisible:cursorRect animated:YES];
    }
}

5. Adjust visible rect, by setting insets

While editing, parts of your UITableView may be covered by the Keyboard. If the tableviews insets are not adjusted, scrollToCursorForTextView: will not be able to scroll to your cursor, if it is at the bottom of the tableview.

- (void)keyboardWillShow:(NSNotification*)aNotification {
    NSDictionary* info = [aNotification userInfo];
    CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
    
    UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0, kbSize.height, 0.0);
    self.tableView.contentInset = contentInsets;
    self.tableView.scrollIndicatorInsets = contentInsets;
}

- (void)keyboardWillHide:(NSNotification*)aNotification {
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.35];
    UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0, 0.0, 0.0);
    self.tableView.contentInset = contentInsets;
    self.tableView.scrollIndicatorInsets = contentInsets;
    [UIView commitAnimations];
}

And last part:

Inside your view did load, sign up for the Notifications for Keyboard changes through NSNotificationCenter:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}

Please don't get mad at me, for making this answer so long. While not all of it is needed to answer the question, I believe that there are other people who these directly related issues will be helpful to.


UPDATE:

As Dave Haupert pointed out, I forgot to include the rectVisible function:

- (BOOL)rectVisible: (CGRect)rect {
    CGRect visibleRect;
    visibleRect.origin = self.tableView.contentOffset;
    visibleRect.origin.y += self.tableView.contentInset.top;
    visibleRect.size = self.tableView.bounds.size;
    visibleRect.size.height -= self.tableView.contentInset.top + self.tableView.contentInset.bottom;
    
    return CGRectContainsRect(visibleRect, rect);
}

Also I noticed, that scrollToCursorForTextView: still included a direct reference to one of the TextFields in my project. If you have a problem with bodyTextView not being found, check the updated version of the function.

Solution 2

There is a new function to replace sizeWithFont, which is boundingRectWithSize.

I added the following function to my project, which makes use of the new function on iOS7 and the old one on iOS lower than 7. It has basically the same syntax as sizeWithFont:

    -(CGSize)text:(NSString*)text sizeWithFont:(UIFont*)font constrainedToSize:(CGSize)size{
        if(IOS_NEWER_OR_EQUAL_TO_7){
            NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                              font, NSFontAttributeName,
                                              nil];

            CGRect frame = [text boundingRectWithSize:size
                                              options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                           attributes:attributesDictionary
                                              context:nil];

            return frame.size;
        }else{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
            return [text sizeWithFont:font constrainedToSize:size];
#pragma clang diagnostic pop
        }
    }

You can add that IOS_NEWER_OR_EQUAL_TO_7 on your prefix.pch file in your project as:

#define IOS_NEWER_OR_EQUAL_TO_7 ( [ [ [ UIDevice currentDevice ] systemVersion ] floatValue ] >= 7.0 )

Solution 3

If you're using UITableViewAutomaticDimension I have a really simple (iOS 8 only) solution. In my case it's a static table view, but i guess you could adapt this for dynamic prototypes...

I have a constraint outlet for the text-view's height and I have implemented the following methods like this:

// Outlets

@property (weak, nonatomic) IBOutlet UITextView *textView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *textViewHeight;


// Implementation

#pragma mark - Private Methods

- (void)updateTextViewHeight {
    self.textViewHeight.constant = self.textView.contentSize.height + self.textView.contentInset.top + self.textView.contentInset.bottom;
}

#pragma mark - View Controller Overrides

- (void)viewDidLoad {
    [super viewDidLoad];
    [self updateTextViewHeight];
}

#pragma mark - TableView Delegate & Datasource

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 80;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewAutomaticDimension;
}

#pragma mark - TextViewDelegate

- (void)textViewDidChange:(UITextView *)textView {
    [self.tableView beginUpdates];
    [self updateTextViewHeight];
    [self.tableView endUpdates];
}

But remember: the text view must be scrollable, and you must setup your constraints such that they work for automatic dimension:

  • setup all the view in the cell in relation to each other, with fixed heights (including the text view height, which you will change programatically)
  • the top most view has the top spacing and the bottom most view has the bottom spacing to the super view;

The most basic cell example is:

  • no other views in the cell except the textview
  • 0 margins around all sides of the text view and a predefined height constraint for the text view.

Solution 4

Tim Bodeit's answer is great. I used the code of Simple Solution to correctly get the height of the text view, and use that height in heightForRowAtIndexPath. But I don't use the rest of the answer to resize the text view. Instead, I write code to change the frame of text view in cellForRowAtIndexPath.

Everything is working in iOS 6 and below, but in iOS 7 the text in text view cannot be fully shown even though the frame of text view is indeed resized. (I'm not using Auto Layout). It should be the reason that in iOS 7 there's TextKit and the position of the text is controlled by NSTextContainer in UITextView. So in my case I need to add a line to set the someTextView in order to make it work correctly in iOS 7.

    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) {
        someTextView.textContainer.heightTracksTextView = YES;
    }

As the documentation said, what that property does is:

Controls whether the receiver adjusts the height of its bounding rectangle when its text view is resized. Default value: NO.

If leave it with the default value, after resize the frame of someTextView, the size of the textContainer is not changed, leading to the result that the text can only be displayed in the area before resizing.

And maybe it is needed to set the scrollEnabled = NO in case there's more than one textContainer, so that the text will reflow from one textContainer to the another.

Solution 5

Here is one more solution that aims at simplicity and quick prototyping:

Setup:

  1. Table with prototype cells.
  2. Each cell contains dynamic sized UITextView w/ other contents.
  3. Prototype cells are associated with TableCell.h.
  4. UITableView is associated with TableViewController.h.

Solution:

(1) Add to TableViewController.m:

 // This is the method that determines the height of each cell.  
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    // I am using a helper method here to get the text at a given cell.
    NSString *text = [self getTextAtIndex:indexPath];

    // Getting the height needed by the dynamic text view.
    CGSize size = [self frameForText:text sizeWithFont:nil constrainedToSize:CGSizeMake(300.f, CGFLOAT_MAX)];

    // Return the size of the current row.
    // 80 is the minimum height! Update accordingly - or else, cells are going to be too thin.
    return size.height + 80; 
}

// Think of this as some utility function that given text, calculates how much 
// space would be needed to fit that text.
- (CGSize)frameForText:(NSString *)text sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size
{
    NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                          font, NSFontAttributeName,
                                          nil];
    CGRect frame = [text boundingRectWithSize:size
                                      options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                   attributes:attributesDictionary
                                      context:nil];

    // This contains both height and width, but we really care about height.
    return frame.size;
}

// Think of this as a source for the text to be rendered in the text view. 
// I used a dictionary to map indexPath to some dynamically fetched text.
- (NSString *) getTextAtIndex: (NSIndexPath *) indexPath
{
    return @"This is stubbed text - update it to return the text of the text view.";
}

(2) Add to TableCell.m:

// This method will be called when the cell is initialized from the storyboard
// prototype. 
- (void)awakeFromNib
{
    // Assuming TextView here is the text view in the cell. 
    TextView.scrollEnabled = YES;
}

Explanation:

So what's happening here is this: each text view is bound to the height of the table cells by vertical and horizontal constraints - that means when the table cell height increases, the text view increases its size as well. I used a modified version of @manecosta's code to calculate the required height of a text view to fit the given text in a cell. So that means given a text with X number of characters, frameForText: will return a size which will have a property size.height that matches the text view's required height.

Now, all that remains is the update the cell's height to match the required text view's height. And this is achieved at heightForRowAtIndexPath:. As noted in the comments, since size.height is only the height for the text view and not the entire cell, there should be some offset added to it. In the case of the example, this value was 80.

Share:
90,750
MyJBMe
Author by

MyJBMe

Updated on October 06, 2020

Comments

  • MyJBMe
    MyJBMe over 3 years

    How can I calculate the height of an UITableViewCell with an UITextView in it in iOS 7?

    I found a lot of answers on similar questions, but sizeWithFont: takes part in every solution and this method is deprecated!

    I know I have to use - (CGFloat)tableView:heightForRowAtIndexPath: but how do I calculate the height my TextView needs to display the whole text?

  • Martin de Keijzer
    Martin de Keijzer almost 11 years
    My UITextViews still don't scale quite well and become scrollable when the text spans 3 lines; pastebin.com/Wh6vmBqh
  • Martin de Keijzer
    Martin de Keijzer almost 11 years
    The second return statement also throws a deprecation warning in XCode.
  • manecosta
    manecosta almost 11 years
    Are you also setting the size of the UItextView to the calculated size of the text, in cellForRowAtIndexPath? Also you shouldn't worry about the warning in the second return as it is only used when the app is ran on an iOS6 device in which the function is not deprecated.
  • MyJBMe
    MyJBMe almost 11 years
    That code is working well at all! It resize everything! But, my TextView always gets a height of 30px! Are there settings I am not allowed to set, or is there something I am not allowed in UITextView?
  • Tim Bodeit
    Tim Bodeit almost 11 years
    Do I understand correctly? Your TableViewCell resizes, but the TextView doesn't? Look in step 3. I have put comments in the place that would be appropriate to put your textView resize code in. Let me know, if you need any more help.
  • Peter Sarnowski
    Peter Sarnowski almost 11 years
    @TimBodeit As I'm updating an app to iOS7 today, I've run into crashes when using [tableview beginUpdates] / [tableview endUpdates] to update cell height - some assertions fail when creating the animation - mainly when the cell is not wholly visible on screen. Have you encountered anything similar?
  • MyJBMe
    MyJBMe almost 11 years
    @TimBodeit No, both resizes, but not enough! I think its because of the attributedText! Is it just init or initWithString for the new AttirbutedString Object? I dont know how to deal with this object!
  • Tim Bodeit
    Tim Bodeit almost 11 years
    @MyJBMe: Attributed String means, that you have to add attributes to it. Just setting your text, like with a regular string is not enough! For example, if you are using Helvetica Bold, font-size 20, you have to add that as an attribute. learn more about attributed strings
  • Tim Bodeit
    Tim Bodeit almost 11 years
    @PeterSarnowski: No, until now, I have not yet had problems with assertion failures in table views. Neither in iOS 6, 7 final or any of the betas.
  • Peter Sarnowski
    Peter Sarnowski almost 11 years
    Tim Bodeit - turns out it was not iOS7 but some old weirdness regarding section footer views.
  • iOS Monster
    iOS Monster almost 11 years
    Doesn't work on iOS 7 for me. Bottom part of the text in text view displayed on the screen.
  • Tim Bodeit
    Tim Bodeit almost 11 years
    @iOSMonster: What do you mean by "bottom part of the text in text view displayed on the screen"? What is your problem?
  • iOS Monster
    iOS Monster almost 11 years
    I mean I can see only last few lines of the text in text view, upper part of the textview hides behind the upper row in the table view.
  • ngb
    ngb almost 11 years
    @PeterSarnowski how did you fix your problem with [tableview beginUpdates] / [tableview endUpdates? i'm having the same problem
  • Peter Sarnowski
    Peter Sarnowski almost 11 years
    @ngb - if you have footer views in your table, try to remove them and check if the problem persists.
  • Vikings
    Vikings over 10 years
    This solution does not seem to work on a copy and paste if the text is large, any ideas?
  • Zorayr
    Zorayr over 10 years
    Can you provide a simple example of how to use this function?
  • MyJBMe
    MyJBMe over 10 years
    What does this 'dream.dream' stands for?
  • Zorayr
    Zorayr over 10 years
    @MyJBMe sorry that was part of my own project - I have updated the code accordingly. dream.dream was the text that I was rendering in the text view.
  • Alexander
    Alexander over 10 years
    @Tim Bodeit, your solution works, thank you! But I think you should note in comment,that assigning of attributedText without specifying font,color and text alignment leads to the setting of default values of NSAttributedString attributes to the textView. In my case it cause getting different heights of the textview for the same text.
  • Alexander
    Alexander over 10 years
  • Vikings
    Vikings over 10 years
    You can fix the copy and paste issue, by adjusting the table view's offset.
  • HpTerm
    HpTerm over 10 years
    @manecosta Apple's documentation says that you must 'ceil' the result : In iOS 7 and later, this method returns fractional sizes (in the size component of the returned CGRect); to use a returned size to size views, you must use raise its value to the nearest higher integer using the ceil function.
  • Chris Wagner
    Chris Wagner over 10 years
    This is great @TimBodeit, but I am having issues with selectedTextRange when textViewDidBeginEditing is called. It always seems to return a range that is equivalent with the end of the string even if I tap at the top of the text view. The cursor gets placed properly but it seems selectedTextRange is inaccurate when the delegate is fired. This causes the cursor to be placed at the top and the view to scroll all the way to the bottom. Thoughts?
  • Richard Venable
    Richard Venable over 10 years
    This is one of my all time favorite Stack Overflow answers - thanks!
  • Wilmar
    Wilmar over 10 years
    @MartindeKeijzer The answer has been updated with #pragma marks to disable the warning, since a manual iOS version check is being done.
  • Danny Xu
    Danny Xu about 10 years
    This is the only right answer here. And guys, take care of the size returned by sizeThatFits:, because even you give the specific 'width' you want it to fit, sizeThatFits: will still return different width(This may cause - content moving up and down when typing). So set the returned size back to UITextView will solve the bug.
  • Hebbian
    Hebbian about 10 years
    I've tried this, and it's not working. Apparently, method in heightForRowAtIndexPath is called first before cellForRowAtIndexPath, which means iVar textViews array always has 0 index at textViewHeightForRowAtIndexPath method. Anyone?
  • Tim Bodeit
    Tim Bodeit about 10 years
    @Hebbian have a look at textViewHeightForRowAtIndexPath:. If there is no corresponding entry in the textViews array, a temporary UITextView will be created for the operation.
  • Duncan Babbage
    Duncan Babbage about 10 years
    Oh my. I love Stack Overflow. :)
  • Bhagyashree
    Bhagyashree almost 10 years
    Thank you very much. It is very helpful.
  • aramusss
    aramusss almost 10 years
    This answer was very helpful, but then i faced the problem that UITextViews with large text in it makes it cut some of the text.. Seems like an iOS 7/8 bug. Using an UILabel solved this!
  • CharlesA
    CharlesA over 9 years
    One small correction: In textViewHeightForRowAtIndexPath you are defining CGFloat textViewWidth twice - which means it gets ignored when !calculationView.attributedText. Otherwise great code!
  • Tim Bodeit
    Tim Bodeit over 9 years
    @CharlesA Fixed. Thanks a lot!
  • Vibhor Goyal
    Vibhor Goyal over 9 years
    In my opinion, the above answer can be improved by replacing [self scrollToCursorForTextView:textView]; with [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES]; It would eliminate the step 4 and provides much more subtle animation.
  • Tim Bodeit
    Tim Bodeit over 9 years
    @VibhorGoyal No, actually this will not do. It probably would ensure that a small row is completely on screen at all times (not tested). Because I haven't tried it, I also can't say if the animation would look better or not. However, my sample code allows for unlimited amounts of text to be entered. If the user chooses to enter enough text for the row to become higher than the height of the screen between top bar and keyboard, your suggestion would stop working and the user wouldn't see what he is typing anymore.
  • Andy
    Andy over 9 years
    Please note: Yes, NSIndexPath can be used for dictionary keys, but, the NSIndexPath's created by tableView delegates sometimes won't work correctly when used with NSDictionary. No idea why, but the solution in the following link fixed it for me: stackoverflow.com/a/21685611/1204153
  • Arun Gupta
    Arun Gupta about 9 years
    @TimBodeit: I am not able to get this working on iOS8. Please let me know how can this be fixed.
  • Tim Bodeit
    Tim Bodeit about 9 years
    @ArunGupta What exactly is your problem?
  • Arun Gupta
    Arun Gupta about 9 years
    On iOS8, when i tap on any textview inside tableview it scrolls to the top and then scrolls back to current position(jumps up and down) and for last row in table view the textview is not even visible on the screen it jump somewhere else
  • Arun Gupta
    Arun Gupta about 9 years
    @TimBodeit Did you get a chance to look at this on iOS8?
  • Chris Shields
    Chris Shields over 8 years
    @TimBodeit: This is thread is a touch old now, but I was just curious about 1 thing. In step 2 you grab cell.textView and I know textView is not a default property of UITableViewCell so I thought ok, you've probably just subclassed UITableViewCell and added this property. But when you dequeue your cell you should be getting a UITableViewCell object in return and not your subclassed cell, should there not be a cast? Unless you intended for that to happen in the //configure cell. Just curious about it. :) thanks for the awesome info!
  • Tim Bodeit
    Tim Bodeit over 8 years
    @ChrisShields True, the UITableViewCell type might be a bit misleading. Maybe something like TableViewCellSubclass would have been clearer.
  • Akshit Zaveri
    Akshit Zaveri about 8 years
    the text view must NOT be scrollable
  • Dvole
    Dvole about 8 years
    I'm getting the same size under updateTextviewHeight all the time. Looks like content size is wrong. Scrolling is disabled.
  • iPeter
    iPeter about 7 years
    Can this be used for UITextField?