简体   繁体   中英

Inter-cell layout logic in iOS table/collection views

What is the idiomatic way to align portions of a separate cells in a UITableView or UICollectionView with a separate row for headers?

For example, suppose I want to display something like this:

Name       Number
Bob        34587
Jane       32489
Barbara    23766
Montgomery 34892

The "Name" and "Number" bits would be in a header cell of some sort. I guess I could use a section header for this (with a single section).

Each cell below the header would need to size intelligently to its content, but that size would need to effect the layout of all other cells.

How is this usually achieved in iOS?

If I understand you correctly, then you'd like the second column to move as close as you can to the first column without braking the content in the first column.

As I know it this is not commonly used on iOS because it is much easier to use fixed width space for both columns and usually there is no need to compress them horizontally.

However if I'd really need to accomplish this then:

  1. I'd run in cycle through all of my cells content and determine the size of the left content by calling NSString method sizeWithFont:contrainedToSize: (if using before iOS7 or boundingRectWithSize:options:attributes:context: if on iOS7).
  2. I'd store the largest value in instance variable (for example _maxLeftContentWidth ).
  3. In my -tableView:cellForRowAtIndexPath: I'd set the frames for my left cell content and right cell content according to my stored instance variable ( _maxLeftContentWidth ).
  4. Whenever the content changed for my cells I'd run the left cell width calculation method again and then call [self.tableView reloadData] to redraw the cells.

Even though it is called a 'table view' it is not really a table how we usually think of tables ( ie , multiple rows with multiple columns). If you really want multiple columns it is up to you. To line them up you have to do the work of figuring out the needed width, or choose a width that is big enough for all cases.

I think I would do something like this:

#define COLUMN_SPACE 10
- (UITableViewCell) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath {
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: @"cell"];
    // Layout the cell
    NSString *name = [myDataStore nameForIndexPath: indexPath];
    NSNumber *number = [myDataStore numberforIndexPath: indexPath];

    CGRect frame = cell.nameLabel.frame;
    frame.size.width = [self nameColumnWidth];
    cell.nameLabel.frame = frame;
    frame = cell.numberLabel.frame;
    frame.origin.x = cell.nameLabel.origin.x + cell.nameLabel.size.width + COLUMN_SPACE;
    frame.size.width = [self numberColumnWidth];
    cell.numberLabel.frame = frame;

    cell.nameLabel.text = name;
    // You would probably want to use an NSNumberFormatter here
    cell.numberLabel.text = [NSString stringWithFormat: @"%d", number.intValue];

    return cell;
}

- (CGFloat) nameColumnWidth {
    if( _nameColumnWidth <= 0.0 ) {
        _nameColumnWidth = 0.0;
        for( NSInteger s = 0; s < [myDataStore numberOfSections]; ++s ) {
            for( NSInteger r = 0; r < [myDataStore numberOfRowsInSection: s]; ++r ) {
                NSIndexPath *path = [NSIndexPath indexPathForRow: r inSection: s];
                NSString *name = [myDataStore nameForIndexPath: indexPath];
                CGFloat widthForName = [name widthNeeded];
                if( widthForName > _nameColumnWidth )
                    _nameColumnWidth = widthForName;
            }
        }
    }
    return _nameColumnWidth;
}

- (CGFloat) numberColumnWidth {
    if( _numberColumnWidth <= 0.0 ) {
        _numberColumnWidth = 0.0;
        for( NSInteger s = 0; s < [myDataStore numberOfSections]; ++s ) {
            for( NSInteger r = 0; r < [myDataStore numberOfRowsInSection: s]; ++r ) {
                NSIndexPath *path = [NSIndexPath indexPathForRow: r inSection: s];
                NSNumber *number = [myDataStore numberforIndexPath: indexPath];
                // You would probably want to use an NSNumberFormatter here
                // (in fact you want to do the same is you do in cellForRowAtIndexPath)
                NSString *numberAsString = [NSString stringWithFormat: @"%d", number.intValue];
                CGFloat widthForNumber = [numberAsString widthNeeded];
                if( widthForNumber > _numberColumnWidth )
                    _numberColumnWidth = widthForNumber;
            }
        }
    }
    return _numberColumnWidth;
}

I would use the same [self nameColumnWidth] and [self numberColumnWidth] when generating the view for the section header, or I would just make the cell at row 0 of the section have the Name and Number headers.

For the width I would extend NSString with this:

@implementation NSString (WithWidthNeeded)
- (CGFloat) widthNeeded {
    // Either set the font you will use here, add it as a parameter, etc.
    UIFont *font = [UIFont systemFontOfSize: 18];
    CGSize maximumLabelSize = CGSizeMake( 9999, font.lineHeight );
    CGSize expectedLabelSize;
    if( [self respondsToSelector: @selector(boundingRectWithSize:options:attributes:context:)] ) {
        CGRect expectedLabelRect = [self boundingRectWithSize: maximumLabelSize options: NSStringDrawingUsesLineFragmentOrigin attributes: @{ NSFontAttributeName: font } context: nil];
            expectedLabelRect = CGRectIntegral( expectedLabelRect );
            expectedLabelSize = expectedLabelRect.size;
    } else {
        NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString: self attributes: @{ NSFontAttributeName: font }];
        if( [attributedText respondsToSelector: @selector(boundingRectWithSize:options:context:)] ) {
            CGRect expectedLabelRect = [attributedText boundingRectWithSize: maximumLabelSize options: NSStringDrawingUsesLineFragmentOrigin context: nil];
            expectedLabelRect = CGRectIntegral( expectedLabelRect );
            expectedLabelSize = expectedLabelRect.size;
    } else {
            expectedLabelSize = [self sizeWithFont: font
                                 constrainedToSize: maximumLabelSize
                                     lineBreakMode: NSLineBreakByTruncatingTail];

        }
    }
    return expectedLabelSize.width;
}
@end

When the data are changed the _nameColumnWidth and _numberColumnWidth can be changed to 0.0 and they will be recalculated so the column widths would be adjusted.

An alternative is to keep track of the maximum widths of the name and number when the cells are layed out in cellForRowAtIndexPath . If they increase, trigger the visible cells to be redrawn. This would have better performance, since it would not have to traverse the entire data set to find the longest name and biggest number . This would, however, result in the columns shifting as you scrolled the view down through the data and longer names or bigger numbers are encountered, unless the longest name and biggest number happened to be in the top row.

It would also be possible to do a kind of hybrid where the nameColumnWidth and numberColumnWidth are set to the maximum visible (sort of a local maximum) but then a background thread is started to go through the rest of the data and find the absolute maximums. When the absolute maximums are found the visible cells are reloaded. Then there is only one shift or change in the column width. (The background thread might not be safe if sizeWithFont: is being called since it is in UIKit . With iOS 6 or 7 I think it would be OK)

Again, since a UITableView is really a UIColumnView it leaves quite a bit of work for you to make multiple columns that can adjust in width for their content.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM