Auto-resizing of DataGrid Row for Columns Using SDataGridMultiLineTextCells


#1

Greetings Programs!

Once you’re finished editing a multiline text cell, it will only show the text available for the height of the row and will not expand to contain the full text. In order to resize the row, you will need to manually do this yourself.

I’ve used this process for resizing UIWebViews in UITableViewCells and it works well here.

Basically, I store the row height in a dictionary which I return in heightForRowAtIndex (or a default height):

@property (strong, nonatomic) NSMutableDictionary *rowHeights;

Here are the methods that you will need (hopefully the comments explain what’s going on):

- (bool)setRowHeightOfCell:(SDataGridMultiLineTextCell *)cell
{
    // The cell title.
    NSString *cellTitle = cell.coordinate.column.title;
    
    // The row index.
    NSString *rowKey = [[NSNumber numberWithInteger:cell.coordinate.row.rowIndex] stringValue];
    
    // Dictionary key for the current height of the row.
    NSString *currentHeightKey = [NSString stringWithFormat:@"currentHeight%@", rowKey];
    
    // Dictionary key for the current column that the height was calculated for.
    NSString *currentColumnKey = [NSString stringWithFormat:@"currentColumn%@", rowKey];
    
    // Dictionary key for the previous height of the row.
    NSString *previousHeightKey = [NSString stringWithFormat:@"previousHeight%@", rowKey];
    
    // Dictionary key for the previous column that the height was calculated for.
    NSString *previousColumnKey = [NSString stringWithFormat:@"previousColumn%@", rowKey];
    
    // The title of the current calculated column.
    NSString *currentColumn = [self.rowHeights valueForKey:currentColumnKey];
    
    // The title of the previous calculated column.
    NSString *previousColumn = [self.rowHeights valueForKey:previousColumnKey];
    
    // The current height of the row.
    float currentHeight = [[self.rowHeights objectForKey:currentHeightKey] floatValue];
    
    // The previous height of the row.
    // This value will be the height of the second highest cell in the row.
    float previousHeight = [[self.rowHeights objectForKey:previousHeightKey] floatValue];

    // The amount of padding between the cell border and the label.
    // Modify as necessary to get the right number of lines.
    CGFloat padding = (cell.contentFrame.size.width - cell.textLabel.bounds.size.width) * 2;

    // The width of the cell text label to use for calculating the height of the label.
    CGFloat width = cell.textLabel.bounds.size.width-padding;
    
    // Calculate the height of the label for the width and text content.
    float newHeight = [self heightForText:cell.text andWidth:width];

    // If the height hasn't changed or if the height of the passed cell is less than the
    // current height of the row, ignore and return false that no new height was set.
    if (newHeight == currentHeight || (newHeight < currentHeight && ![currentColumn isEqualToString:cellTitle]))
    {
        // Update the previous height if the new height is greater or if it's the same column.
        // We always want the previous height to be the second highest height.
        if (newHeight > previousHeight || (newHeight < previousHeight && [previousColumn isEqualToString:cellTitle]))
        {
            // Save the new row height as the previous row height.
            [self.rowHeights setObject:[NSNumber numberWithFloat:newHeight] forKey:previousHeightKey];
            
            // Save the current column title as the previous column title.
            [self.rowHeights setValue:cellTitle forKey:previousColumnKey];
        }

        return false;
    }
    
    // If the new height is less than the previous height then make the new height and column the
    // previous height and column since we do not want to resize it less than the second highest cell in the row.
    if (newHeight < previousHeight)
    {
        // Save the previous row height as the current row height.
        [self.rowHeights setObject:[NSNumber numberWithFloat:previousHeight] forKey:currentHeightKey];
        
        // Save the previous column title as the current column title.
        [self.rowHeights setValue:previousColumn forKey:currentColumnKey];
        
        // Return true that a new height was set.
        return true;
    }
    
    // If the passed cell column is different from the current column and the height is
    // is greater than the current height, move the current row height and column values to the previous
    // height and column then set the current height and column values to those of the passed cell.
    //
    // or
    //
    // If the passed cell column is the same as the current column and the height is
    // less than the current column then update the height and column only.
    if (newHeight > currentHeight || (newHeight < currentHeight && [currentColumn isEqualToString:cellTitle]))
    {
        if (![currentColumn isEqualToString:cellTitle]) {
            // Save the current row height as the previous row height.
            [self.rowHeights setObject:[NSNumber numberWithFloat:currentHeight] forKey:previousHeightKey];
            
            // Save the current column title as the previous column title.
            [self.rowHeights setValue:currentColumn forKey:previousColumnKey];
        }
        
        // Save the title of the column used to calculate the new height.
        [self.rowHeights setValue:cellTitle forKey:currentColumnKey];
        
        // Save the new calculated height as the current height of the row.
        [self.rowHeights setObject:[NSNumber numberWithFloat:newHeight] forKey:currentHeightKey];
        
        // Return true that a new height was set.
        return true;
    }
    
    // Return false that no new height was set.
    return false;
}

Calculating the height for the text and label width:

// Modified from StackOverflow:
// http://stackoverflow.com/questions/18368567/uitableviewcell-with-uitextview-height-in-ios-7
//
- (CGFloat)heightForText: (NSString *)text andWidth: (CGFloat)width {
    UITextView *calculationView = [[UITextView alloc] init];
    [calculationView setText:text];
    CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
    return size.height;
}

Delegate method for heightForRowAtIndex:

- (NSNumber *)shinobiDataGrid:(ShinobiDataGrid *)grid heightForRowAtIndex:(NSInteger)rowIndex inSection:(NSInteger)sectionIndex
{
    // Dictionary key for the current height of the row.
    NSString *currentHeightKey = [NSString stringWithFormat:@"currentHeight%@", [[NSNumber numberWithInteger:rowIndex] stringValue]];
    
    // The default (minimum) height required for the rows.
    float minHeight = 50.0;
    
    // The saved height for the row.
    float rowHeight = [[self.rowHeights objectForKey:currentHeightKey] floatValue];
    
    // If the saved height is greater than the minimum height, return it otherwise
    // return the minimum height.
    return [NSNumber numberWithFloat:(rowHeight > minHeight ? rowHeight : minHeight)];
}

Using the HandlingEditing sample app, I made the Forename and Surname cells SDataGridMultiLineTextCell and in didFinishEditingCellAtCoordinate I called setRowHeightOfCell after updating these properties:

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid didFinishEditingCellAtCoordinate:(SDataGridCoord *)coordinate {
{
    // Lots of code here; removed for brevity.

    // Used to determine if we need to reload the grid after setting the height.
    bool reloadGrid = false;
    
    // Determine which column this cell belongs to
    if ([cellTitle isEqualToString:@"Forename"]) {
        // Cast cell as SDataGridMultiLineTextCell, set property.

        // Do something only if the value has changed.
        if (![person.forename isEqualToString:cell.text]) {
            person.forename = cell.text;
            
            // Set the row height.
            reloadGrid = [self setRowHeightOfCell:cell];
        }
    } else if ([cellTitle isEqualToString:@"Surname"]) {
        // Cast cell as SDataGridMultiLineTextCell, set property.

        // Do something only if the value has changed.
        if (![person.surname isEqualToString:cell.text]) {
            person.surname = cell.text;
            
            // Set the row height.
            reloadGrid = [self setRowHeightOfCell:cell];
        }
    }
    else if ([cellTitle isEqualToString:@"Age"]) {
        // Cast cell as SDataGridTextCell and do stuff...
    }
    
    // If a new height was set then reload the datagrid.
    if (reloadGrid) {
        [_shinobiDataGrid reload];
    }
}

And that’s it… well, almost…

This works great when you’re editing cells but what if the rows need to be resized for your initial data?

Using the same HandlingEditing sample app, in heightForRowAtIndex, change the initial rowHeight assignment to:

// The saved height for the row.
    float rowHeight = [[self.rowHeights objectForKey:currentHeightKey] floatValue];
	
    // If the rowHeight is 0 then a multiline text cell has not been edited in this row.
    if (rowHeight == 0)
    {
        // Try to set the height for the row from the current data in the row.
        rowHeight = [self setRowHeightAtIndex:rowIndex];
    }

And the method:

- (float)setRowHeightAtIndex:(NSInteger)rowIndex
{
    // The row index.
    NSString *rowKey = [[NSNumber numberWithInteger:rowIndex] stringValue];
	
    // Dictionary key for the current height of the row.
    NSString *currentHeightKey = [NSString stringWithFormat:@"currentHeight%@", rowKey];
	
    // Dictionary key for the current column that the height was calculated for.
    NSString *currentColumnKey = [NSString stringWithFormat:@"currentColumn%@", rowKey];
	
    // Dictionary key for the previous height of the row.
    NSString *previousHeightKey = [NSString stringWithFormat:@"previousHeight%@", rowKey];
	
    // Dictionary key for the previous column that the height was calculated for.
    NSString *previousColumnKey = [NSString stringWithFormat:@"previousColumn%@", rowKey];

    PersonDataObject *person = (PersonDataObject *)self.data[rowIndex];

    // Use the property with the greatest number of characters.
    // Not the best way if the font sizes are different or have attributed text but will
    // work well if all are the same.
    //
    // Values for the current row height and column.
    NSString *cellTitle = person.forename.length > person.surname.length ? @"Forename" : @"Surname";
    NSString *text = person.forename.length > person.surname.length ? person.forename : person.surname;
    CGFloat width = person.forename.length > person.surname.length ? 118 : 118;

    // Values for the previous height.
    NSString *previousCellTitle = person.forename.length < person.surname.length ? @"Forename" : @"Surname";
    NSString *previousText = person.forename.length < person.surname.length ? person.forename : person.surname;
    CGFloat previousWidth = person.forename.length < person.surname.length ? 118 : 118;
	
    // Calculate the height of the label for the width and text.
    CGFloat currentHeight = [self heightForText:text andWidth:width];
    CGFloat previousHeight = [self heightForText:previousText andWidth:previousWidth];
	
    // Save the title of the columns used to calculate the current and previous heights in the row.
    [self.rowHeights setValue:cellTitle forKey:currentColumnKey];
    [self.rowHeights setValue:previousCellTitle forKey:previousColumnKey];

    // Save the calculated heights for the current and previous heights of the row.
    [self.rowHeights setObject:[NSNumber numberWithFloat:currentHeight] forKey:currentHeightKey];
    [self.rowHeights setObject:[NSNumber numberWithFloat:previousHeight] forKey:previousHeightKey];
	
    return currentHeight;
}

For the row at that index that’s about to be displayed, it will figure out which columns are the current and previous (whichever has the greater height), calculate the heights for both and then set the height and column values for them. Problem with this is that the greater number of multiline text columns you have, the more complicated (and dirty) this method becomes.

I don’t know how optimized the code is so if you find ways to make it better please let me know.

Anyways, I hope it helps as a starting point for anyone in the same boat.

Wg