402

I'm using a UITableView in my iPhone app, and I have a list of people that belong to a group. I would like it so that when the user clicks on a particular person (thus selecting the cell), the cell grows in height to display several UI controls for editing the properties of that person.

Is this possible?

Mark Amery
  • 110,735
  • 57
  • 354
  • 402

21 Answers21

884

I found a REALLY SIMPLE solution to this as a side-effect to a UITableView I was working on.....

Store the cell height in a variable that reports the original height normally via the tableView: heightForRowAtIndexPath:, then when you want to animate a height change, simply change the value of the variable and call this...

[tableView beginUpdates];
[tableView endUpdates];

You will find it doesn't do a full reload but is enough for the UITableView to know it has to redraw the cells, grabbing the new height value for the cell.... and guess what? It ANIMATES the change for you. Sweet.

I have a more detailed explanation and full code samples on my blog... Animate UITableView Cell Height Change

Marc
  • 5,284
  • 5
  • 26
  • 50
Simon Lee
  • 22,224
  • 4
  • 38
  • 44
  • 3
    It's better than [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES]; – Mr. Ming Jun 30 '11 at 11:26
  • Simon Lee's answer works great. Just keep an array of rowHeights. Change the one you want and then do his beginUpdates/endUpdates trick. – user256174 Jan 21 '10 at 20:50
  • 7
    This is brilliant. But is there any way we can control the animation speed of the table? – Josh Kahane Nov 23 '12 at 14:43
  • 7
    This works but if I make a cell larger lets say from 44 to 74 and then make it smaller again to 44 the separator line acts totally weird. Can some one confirm this? – plaetzchen Jan 23 '13 at 17:29
  • 48
    This is a bizarre solution, but it's what Apple recommends in the WWDC 2010 "Mastering Table View" session as well. I'm going to file a bug report on adding this to the documentation because I've just spent about 2 hours researching. – bpapa Feb 21 '13 at 17:32
  • 2
    If you are using an UITextView within the cell that resizes the cell while you type, you need to scroll the textView to the top after the tableView endUpdates: [textView setContentOffset:CGPointMake(0, 0) animated:YES]; – Obiwahn May 11 '13 at 16:53
  • 1
    cellForRowAtIndexPath wont get called ? – Amogh Talpallikar Jun 25 '13 at 15:04
  • 5
    I have tried this solution and it only works sometimes, when you press a cell there's 50% probability to work. Anyone have the same bug? Its because iOS7? – Joan Cardona Jan 17 '14 at 12:16
  • 2
    I've noticed that the wrapping update calls into UIView block-based animation produces more smooth result. Like `[UIView animateWithDuration:0.3 animations:^{ [self.tableView beginUpdates]; /* change some logic */ [self.tableView endUpdates]; }];` – zubko Jan 23 '14 at 16:13
  • 2
    I just want to add that you should *not* call `reloadRowsAtIndexPaths:` if you use static table cells because of the reason (I consider this a bug) mentioned here: http://stackoverflow.com/a/18713132/238948 – devios1 Sep 03 '14 at 17:58
  • 1
    For some unknown reason I had to use `[indexPath isEqual:selectedIndexPath]` instead of `==` inside `heightForRowAtIndexPath`. Not quite sure why since I was able to use `==` inside `didSelectRowAtIndexPath`. Anyway keep that in mind. – Johann Burgess May 25 '15 at 11:25
  • Ludicrous solution, but it works. I just spent I don't know how long trying to achieve this with UIAnimation, reloadSection, reloadData etc. This just does everything I wanted. – nickdnk Jun 08 '15 at 16:10
  • @Benjohn You are right that the tableview may jump while scrolling up. Removing estimatedRowHeight resolves this problem! – trudger Jul 01 '15 at 07:53
  • 8
    Now it is also written in the official documentation: `You can also use this method followed by the endUpdates method to animate the change in the row heights without reloading the cell.` https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/index.html – Jaroslav Oct 27 '15 at 09:47
  • If anyone with iOS 10 is having troubles with the cell jumping instead of smoothly animating using this method, simply incorporate this code with the answer here: http://stackoverflow.com/questions/39104846/uitableviewcell-animate-height-issue-in-ios-10 – Edwin Finch Nov 01 '16 at 06:18
  • I'm having an issue where this is animated correctly only when I expand, but not when I collapse the cell. The content inside the cell is animated correctly, but the cell simply gets the collapsed cell size instantly and then the subviews are animated into correct place. Anyone else recognizing this? – ClockWise May 31 '17 at 10:36
  • 1
    animation works perfect but i need to scroll the tableview also that is causing the glitch any idea? – tryKuldeepTanwar Oct 30 '17 at 13:05
  • Thanks... You saved my day. – iranjith4 Dec 16 '17 at 03:56
  • Saved my day !! – Clement Joseph Sep 10 '18 at 09:11
  • I think there's an issue here having to do with cell reuse. If the new height of the cell is small enough that it "pulls" up cells from the bottom that had not been dequeued, then simply calling endUpdates() does nothing to dequeue the cells and they will appear blank. – John Scalo May 18 '20 at 18:41
64

I like the answer by Simon Lee. I didn't actually try that method but it looks like it would change the size of all the cells in the list. I was hoping for a change of just the cell that is tapped. I kinda did it like Simon but with just a little difference. This will change the look of a cell when it is selected. And it does animate. Just another way to do it.

Create an int to hold a value for the current selected cell index:

int currentSelection;

Then:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    int row = [indexPath row];
    selectedNumber = row;
    [tableView beginUpdates];
    [tableView endUpdates];
}

Then:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    if ([indexPath row] == currentSelection) {
        return  80;
    }
    else return 40;


}

I am sure you can make similar changes in tableView:cellForRowAtIndexPath: to change the type of cell or even load a xib file for the cell.

Like this, the currentSelection will start at 0. You would need to make adjustments if you didn't want the first cell of the list (at index 0) to look selected by default.

Alexander
  • 22,498
  • 10
  • 56
  • 73
Mark A.
  • 1,065
  • 10
  • 15
  • 2
    Check the code attached to my post, I did exactly this, double the height for a cell when selected. :) – Simon Lee Jun 18 '11 at 19:23
  • 8
    "I didn't actually try that method but it looks like it would change the size of all the cells in the list" - then you didn't look very hard. – jamie Aug 11 '11 at 09:27
  • 14
    The current selection is already stored in tableView.indexPathForSelectedRow. – Nick Sep 24 '13 at 21:39
22

Add a property to keep track of the selected cell

@property (nonatomic) int currentSelection;

Set it to a sentinel value in (for example) viewDidLoad, to make sure that the UITableView starts in the 'normal' position

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    //sentinel
    self.currentSelection = -1;
}

In heightForRowAtIndexPath you can set the height you want for the selected cell

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    int rowHeight;
    if ([indexPath row] == self.currentSelection) {
        rowHeight = self.newCellHeight;
    } else rowHeight = 57.0f;
    return rowHeight;
}

In didSelectRowAtIndexPath you save the current selection and save a dynamic height, if required

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
        // do things with your cell here

        // set selection
        self.currentSelection = indexPath.row;
        // save height for full text label
        self.newCellHeight = cell.titleLbl.frame.size.height + cell.descriptionLbl.frame.size.height + 10;

        // animate
        [tableView beginUpdates];
        [tableView endUpdates];
    }
}

In didDeselectRowAtIndexPath set the selection index back to the sentinel value and animate the cell back to normal form

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {       
        // do things with your cell here

        // sentinel
        self.currentSelection = -1;

        // animate
        [tableView beginUpdates];
        [tableView endUpdates];
    }
}
mrgrieves
  • 477
  • 4
  • 18
Joy
  • 221
  • 2
  • 2
  • Thank you, thank you, thank you! I added a bit of code to make the cell toggle-able. I added the code below. – Septronic Nov 19 '15 at 20:54
15

Instead of beginUpdates()/endUpdates(), the recommended call is now:

tableView.performBatchUpdates(nil, completion: nil)

Apple says, regarding beginUpdates/endUpdates: "Use the performBatchUpdates(_:completion:) method instead of this one whenever possible."

See: https://developer.apple.com/documentation/uikit/uitableview/1614908-beginupdates

raf
  • 2,339
  • 17
  • 19
14

reloadData is no good because there's no animation...

This is what I'm currently trying:

NSArray* paths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView deleteRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];

It almost works right. Almost. I'm increasing the height of the cell, and sometimes there's a little "hiccup" in the table view as the cell is replaced, as if some scrolling position in the table view is being preserved, the new cell (which is the first cell in the table) ends up with its offset too high, and the scrollview bounces to reposition it.

lawrence
  • 8,129
  • 5
  • 36
  • 49
  • Personally I found that using this method but with UITableViewRowAnimationNone provides a smoother but still not perfect result. – Ron Srebro Jan 08 '10 at 03:29
11

I don't know what all this stuff about calling beginUpdates/endUpdates in succession is, you can just use -[UITableView reloadRowsAtIndexPaths:withAnimation:]. Here is an example project.

axiixc
  • 1,943
  • 18
  • 25
  • Excellent this doesn't stretch my text views in my auto layout cell. But it does have to have a flicker to the animation because the None animation option looks glitchy when updating the cell size. – h3dkandi Jul 12 '17 at 14:30
10

I resolved with reloadRowsAtIndexPaths.

I save in didSelectRowAtIndexPath the indexPath of cell selected and call reloadRowsAtIndexPaths at the end (you can send NSMutableArray for list of element's you want reload).

In heightForRowAtIndexPath you can check if indexPath is in the list or not of expandIndexPath cell's and send height.

You can check this basic example: https://github.com/ferminhg/iOS-Examples/tree/master/iOS-UITableView-Cell-Height-Change/celdascambiadetam It's a simple solution.

i add a sort of code if help you

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath: (NSIndexPath*)indexPath
{
    if ([indexPath isEqual:_expandIndexPath])
        return 80;

    return 40;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Celda";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    [cell.textLabel setText:@"wopwop"];

    return cell;
}

#pragma mark -
#pragma mark Tableview Delegate Methods

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *modifiedRows = [NSMutableArray array];
    // Deselect cell
    [tableView deselectRowAtIndexPath:indexPath animated:TRUE];
    _expandIndexPath = indexPath;
    [modifiedRows addObject:indexPath];

    // This will animate updating the row sizes
    [tableView reloadRowsAtIndexPaths:modifiedRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
fermin
  • 566
  • 6
  • 24
5

Swift 4 and Above

add below code into you tableview's didselect row delegate method

tableView.beginUpdates()
tableView.setNeedsLayout()
tableView.endUpdates()
midhun p
  • 1,351
  • 11
  • 20
3

Try this is for expanding indexwise row:

@property (nonatomic) NSIndexPath *expandIndexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath
{
if ([indexPath isEqual:self.expandedIndexPath])
    return 100;

return 44;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSMutableArray *modifiedRows = [NSMutableArray array];
if ([indexPath isEqual:self.expandIndexPath]) {
    [modifiedRows addObject:self.expandIndexPath];
    self.expandIndexPath = nil;
} else {
    if (self.expandedIndexPath)
        [modifiedRows addObject:self.expandIndexPath];

    self.expandIndexPath = indexPath;
    [modifiedRows addObject:indexPath];
}

// This will animate updating the row sizes
[tableView reloadRowsAtIndexPaths:modifiedRows withRowAnimation:UITableViewRowAnimationAutomatic];

// Preserve the deselection animation (if desired)
[tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ViewControllerCellReuseIdentifier];
    cell.textLabel.text = [NSString stringWithFormat:@"I'm cell %ld:%ld", (long)indexPath.section, (long)indexPath.row];

return cell;
}
Nagarjun
  • 5,811
  • 5
  • 28
  • 47
3
BOOL flag;

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    flag = !flag;
    [tableView beginUpdates];
    [tableView reloadRowsAtIndexPaths:@[indexPath] 
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView endUpdates];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return YES == flag ? 20 : 40;
}
Roman Solodyashkin
  • 741
  • 10
  • 13
2

just a note for someone like me searching for add "More Details" on custom cell.

[tableView beginUpdates];
[tableView endUpdates];

Did a excellent work, but don't forget to "crop" cell view. From Interface Builder select your Cell -> Content View -> from Property Inspector select "Clip subview"

2

Heres a shorter version of Simons answer for Swift 3. Also allows for toggling of the cell's selection

var cellIsSelected: IndexPath?


  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    cellIsSelected = cellIsSelected == indexPath ? nil : indexPath
    tableView.beginUpdates()
    tableView.endUpdates()
  }


  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    if cellIsSelected == indexPath {
      return 250
    }
    return 65
  }
aBikis
  • 325
  • 4
  • 15
1

Swift Version of Simon Lee's answer .

// MARK: - Variables 
  var isCcBccSelected = false // To toggle Bcc.



    // MARK: UITableViewDelegate
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

    // Hide the Bcc Text Field , until CC gets focused in didSelectRowAtIndexPath()
    if self.cellTypes[indexPath.row] == CellType.Bcc {
        if (isCcBccSelected) {
            return 44
        } else {
            return 0
        }
    }

    return 44.0
}

Then in didSelectRowAtIndexPath()

  func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    self.tableView.deselectRowAtIndexPath(indexPath, animated: true)

    // To Get the Focus of CC, so that we can expand Bcc
    if self.cellTypes[indexPath.row] == CellType.Cc {

        if let cell = tableView.cellForRowAtIndexPath(indexPath) as? RecipientTableViewCell {

            if cell.tag == 1 {
                cell.recipientTypeLabel.text = "Cc:"
                cell.recipientTextField.userInteractionEnabled = true
                cell.recipientTextField.becomeFirstResponder()

                isCcBccSelected = true

                tableView.beginUpdates()
                tableView.endUpdates()
            }
        }
    }
}
ioopl
  • 1,627
  • 16
  • 18
1

Yes It's Possible.

UITableView has a delegate method didSelectRowAtIndexPath

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [UIView animateWithDuration:.6
                          delay:0
         usingSpringWithDamping:UIViewAnimationOptionBeginFromCurrentState
          initialSpringVelocity:0
                        options:UIViewAnimationOptionBeginFromCurrentState animations:^{

                            cellindex = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
                            NSArray* indexArray = [NSArray arrayWithObjects:indexPath, nil];
                            [violatedTableView beginUpdates];
                            [violatedTableView reloadRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationAutomatic];
                            [violatedTableView endUpdates];
                        }
                     completion:^(BOOL finished) {
    }];
}

But in your case if the user scrolls and selects a different cell then u need to have the last selected cell to shrink and expand the currently selected cell reloadRowsAtIndexPaths: calls heightForRowAtIndexPath: so handle accordingly.

Koushik
  • 1,110
  • 14
  • 19
0

Here is my code of custom UITableView subclass, which expand UITextView at the table cell, without reloading (and lost keyboard focus):

- (void)textViewDidChange:(UITextView *)textView {
    CGFloat textHeight = [textView sizeThatFits:CGSizeMake(self.width, MAXFLOAT)].height;
    // Check, if text height changed
    if (self.previousTextHeight != textHeight && self.previousTextHeight > 0) {
        [self beginUpdates];

        // Calculate difference in height
        CGFloat difference = textHeight - self.previousTextHeight;

        // Update currently editing cell's height
        CGRect editingCellFrame = self.editingCell.frame;
        editingCellFrame.size.height += difference;
        self.editingCell.frame = editingCellFrame;

        // Update UITableView contentSize
        self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height + difference);

        // Scroll to bottom if cell is at the end of the table
        if (self.editingNoteInEndOfTable) {
            self.contentOffset = CGPointMake(self.contentOffset.x, self.contentOffset.y + difference);
        } else {
            // Update all next to editing cells
            NSInteger editingCellIndex = [self.visibleCells indexOfObject:self.editingCell];
            for (NSInteger i = editingCellIndex; i < self.visibleCells.count; i++) {
                UITableViewCell *cell = self.visibleCells[i];
                CGRect cellFrame = cell.frame;
                cellFrame.origin.y += difference;
                cell.frame = cellFrame;
            }
        }
        [self endUpdates];
    }
    self.previousTextHeight = textHeight;
}
Vitalii Gozhenko
  • 8,319
  • 2
  • 42
  • 64
0

I used @Joy's awesome answer, and it worked perfectly with ios 8.4 and XCode 7.1.1.

In case you are looking to make your cell toggle-able, I changed the -tableViewDidSelect to the following:

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
//This is the bit I changed, so that if tapped once on the cell, 
//cell is expanded. If tapped again on the same cell, 
//cell is collapsed. 
    if (self.currentSelection==indexPath.row) {
        self.currentSelection = -1;
    }else{
        self.currentSelection = indexPath.row;
    }
        // animate
        [tableView beginUpdates];
        [tableView endUpdates];

}

I hope any of this helped you.

Septronic
  • 1,070
  • 12
  • 29
0

Check this method after iOS 7 and later.

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

Improvements have been made to this in iOS 8. We can set it as property of the table view itself.

David
  • 3,055
  • 1
  • 31
  • 48
Govind
  • 2,252
  • 31
  • 40
0

Swift version of Simon Lee's answer:

tableView.beginUpdates()
tableView.endUpdates()

Keep in mind that you should modify the height properties BEFORE endUpdates().

Tamás Sengel
  • 47,657
  • 24
  • 144
  • 178
0

Inputs -

tableView.beginUpdates() tableView.endUpdates() these functions will not call

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}

But, if you do, tableView.reloadRows(at: [selectedIndexPath! as IndexPath], with: .none)

It will call the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {} this function.

sRoy
  • 267
  • 2
  • 5
-1

I just resolved this problem with a little hack:

static int s_CellHeight = 30;
static int s_CellHeightEditing = 60;

- (void)onTimer {
    cellHeight++;
    [tableView reloadData];
    if (cellHeight < s_CellHeightEditing)
        heightAnimationTimer = [[NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(onTimer) userInfo:nil repeats:NO] retain];
}

- (CGFloat)tableView:(UITableView *)_tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
        if (isInEdit) {
            return cellHeight;
        }
        cellHeight = s_CellHeight;
        return s_CellHeight;
}

When I need to expand the cell height I set isInEdit = YES and call the method [self onTimer] and it animates the cell growth until it reach the s_CellHeightEditing value :-)

Dzamir
  • 145
  • 3
  • 6
  • In the simulator works great, but in the iPhone hardware is laggy. With a 0.05 timer delay and with a cellHeight increase of 5 units, it's much better, but nothing like CoreAnimation – Dzamir Aug 21 '09 at 17:23
  • 1
    Confirm, before post..next time. – ajay_nasa Sep 18 '15 at 13:02
-2

Get the indexpath of the row selected. Reload the table. In the heightForRowAtIndexPath method of UITableViewDelegate, set the height of the row selected to a different height and for the others return the normal row height

lostInTransit
  • 68,087
  • 58
  • 193
  • 270
  • 2
    -1, doesn't work. Calling `[table reloadData]` results in the height change happening instantly, rather than animating. – Mark Amery Jul 28 '13 at 16:43