Anders
Anders

Reputation: 2941

EXC_BAD_ACCESS in heightForRowAtIndexPath iOS

I'm working on an application where I have a custom subclass of UITableViewCell. I want to make the cell's height dynamic based on the text inside it. I try do do that in my heightForRowAtIndexPath method. But I'm having some issues, the following code causes and EXC_BAD_ACCESS(code=2 address=0xb7ffffcc) error.

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

    PostCell *cell = (PostCell *)[tableView cellForRowAtIndexPath:indexPath];

    CGSize postTextSize;
    if (![cell.postText.text isEqualToString:@""]) {
        postTextSize = [cell.postText.text sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(cell.postText.frame.size.width, 200)];
    } else {
        postTextSize = CGSizeMake(cell.postText.frame.size.width, 40);
    }

    return postTextSize.height + cell.userName.frame.size.height + 10;
}

If I instead have:

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

It works.

It seems to be crashing on this line: PostCell *cell = (PostCell *)[tableView cellForRowAtIndexPath:indexPath];, and I don't know why. I have tried to build clean and reset the simulator, but neither worked. Any ideas why this is happening?

Upvotes: 27

Views: 10734

Answers (5)

Philip
Philip

Reputation: 7166

SWIFT:

Use this to know when the tables have loaded:

extension UITableView {
    func reloadData(completion: ()->()) {
        UIView.animateWithDuration(0, animations: { self.reloadData() })
            { _ in completion() }
    }
}

Create a boolean value

var tables_loaded = Bool(false)

Then in viewDidLoad:

tableView.reloadData {
    tables_loaded = true
    // call functions or other stuff regarding table manipulation.
    // tableView.beginUpdates()
    // tableView.endUpdates()
}

then in

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
            if tables_loaded {
                let cell = tableView.cellForRowAtIndexPath(indexPath)
                 // Do your magic here!
             }

}

Upvotes: -1

ninjaproger
ninjaproger

Reputation: 2044

In this case you should call UITableViewDataSource method for getting cell for indexPath instead of cellForRowAtIndexPath: method of UITableView because at the moment when tableView: heightForRowAtIndexPath: first time called there are no any cells in tableView.

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath: (NSIndexPath *)indexPath {
    PostCell *cell = (PostCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];

    CGSize postTextSize;
    if (![cell.postText.text isEqualToString:@""]) {
    postTextSize = [cell.postText.text sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(cell.postText.frame.size.width, 200)];
    } else {
        postTextSize = CGSizeMake(cell.postText.frame.size.width, 40);
    }

    return postTextSize.height + cell.userName.frame.size.height + 10;
}

It works and no causes EXC_BAD_ACCESS exception.

Upvotes: 0

Martin R
Martin R

Reputation: 539815

I have tried to reproduce the problem. It turns out that calling cellForRowAtIndexPath: inside heightForRowAtIndexPath causes heightForRowAtIndexPath to be called recursively. Here is an extract of the stack backtrace after the 3 recursion steps:

frame #0: 0x000042d0 DocInteraction`-[DITableViewController tableView:heightForRowAtIndexPath:] + 48 at DITableViewController.m:262
frame #1: 0x0054f688 UIKit`-[UISectionRowData refreshWithSection:tableView:tableViewRowData:] + 3437
frame #2: 0x0055040f UIKit`-[UITableViewRowData(UITableViewRowDataPrivate) _ensureSectionOffsetIsValidForSection:] + 144
frame #3: 0x00551889 UIKit`-[UITableViewRowData numberOfRows] + 137
frame #4: 0x00553dac UIKit`-[UITableViewRowData globalRowsInRect:] + 42
frame #5: 0x003f82eb UIKit`-[UITableView(_UITableViewPrivate) _visibleGlobalRowsInRect:] + 177
frame #6: 0x004001e6 UIKit`-[UITableView cellForRowAtIndexPath:] + 113
frame #7: 0x000042f2 DocInteraction`-[DITableViewController tableView:heightForRowAtIndexPath:] + 82 at DITableViewController.m:262
frame #8: 0x0054f688 UIKit`-[UISectionRowData refreshWithSection:tableView:tableViewRowData:] + 3437
frame #9: 0x0055040f UIKit`-[UITableViewRowData(UITableViewRowDataPrivate) _ensureSectionOffsetIsValidForSection:] + 144
frame #10: 0x00551889 UIKit`-[UITableViewRowData numberOfRows] + 137
frame #11: 0x00553dac UIKit`-[UITableViewRowData globalRowsInRect:] + 42
frame #12: 0x003f82eb UIKit`-[UITableView(_UITableViewPrivate) _visibleGlobalRowsInRect:] + 177
frame #13: 0x004001e6 UIKit`-[UITableView cellForRowAtIndexPath:] + 113
frame #14: 0x000042f2 DocInteraction`-[DITableViewController tableView:heightForRowAtIndexPath:] + 82 at DITableViewController.m:262
frame #15: 0x0054f688 UIKit`-[UISectionRowData refreshWithSection:tableView:tableViewRowData:] + 3437
frame #16: 0x0055040f UIKit`-[UITableViewRowData(UITableViewRowDataPrivate) _ensureSectionOffsetIsValidForSection:] + 144
frame #17: 0x00551889 UIKit`-[UITableViewRowData numberOfRows] + 137
frame #18: 0x003ff66d UIKit`-[UITableView noteNumberOfRowsChanged] + 119
frame #19: 0x003ff167 UIKit`-[UITableView reloadData] + 764

Finally the program crashes. On my Simulator this happens when the back trace is about 57000 levels deep.


Old answer (not wrong, but does not explain the EXC_BAD_ACCESS):

The problem is that

[tableView cellForRowAtIndexPath:indexPath]

returns nil for rows that are currently not visible. A table view allocates only so many cells that are required to display the currently visible rows. The cells are reused when you scroll the table view.

But heightForRowAtIndexPath is called for all cells of the table view before any row is displayed.

As a consequence, you should not get the text from the table view cells to compute the height in heightForRowAtIndexPath. You should get the text from your data source instead.

Upvotes: 54

magellan_tor
magellan_tor

Reputation: 71

I had a similar problem and came across, just to share what I do now which works well for me

CGFloat mycell_height;

- (void)viewDidLoad
{
    [super viewDidLoad];
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCell"];
    mycell_height = cell.frame.size.height;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
  return mycell_height; //assuming all cells are the same
}

Hope this helps anyone looking for this and new to iOS programming like me :-)

Upvotes: 2

fengd
fengd

Reputation: 7569

tableView heightForRowAtIndexPath is called before cellForRowAtIndexPath, before a cell is displayed, the height needs to be calculated first.

you should get text from your data source, not from cell

Upvotes: 7

Related Questions