0

I've been working on a set of classes that allow for UITableView's and their associated classes to uses auto layout and dynamic type. It's based around an answer on Stack Overflow that aims to add auto layout support.

So far, it works well, but I've run in to a couple of problems when using size classes. The first is directly related to the table calculating the height:

When I create a new UITableViewCell, but don't add it to any views, the size class is AnyxAny, so when I've got some subviews or constraints that change based on the size class, they're always working as if they're in an AnyxAny situation. So far, my very hacky solution is to create a new UIWindow that I add the cells to by:

UIWindow(frame: UIScreen.mainScreen().applicationFrame)

This functions correctly, but I've got a couple of issues with it:

  • I'm now creating an entire new UIWindow object, which seems inefficient
  • Each cell has to be added to the same window
  • When the screen rotates, the size class may change (i.e., iPhone 6 Plus), so I need to listen for changes to the application frame and update my window's frame (not implemented yet)

Is there an easy/more efficient way to ensure the UITableViewCell knows its size class, without having to create a new UIWindow, or do that more efficiently? Maybe I can add a

Most of the current code can be found via the GitHub page, but the most relevant methods are:

DynamicTableViewController

private var cachedClassesForCellReuseIdentifiers = [String : UITableViewCell.Type]()
private var cachedNibsForCellReuseIdentifiers = [String : UINib]()
private var offscreenCellRowsForReuseIdentifiers = [String : UITableViewCell]()

private var offScreenWindow: UIWindow = {
    return UIWindow(frame: UIScreen.mainScreen().applicationFrame)
}()

override public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method is called with an NSMutableIndexPath, which is not compatible with an imutable NSIndexPath,
    // so we create an imutable NSIndexPath to be passed to the following methods
    let imutableIndexPath = NSIndexPath(forRow: indexPath.row, inSection: indexPath.section)

    if let reuseIdentifier = self.cellReuseIdentifierForIndexPath(imutableIndexPath) {
        if let cell = self.cellForReuseIdentifier(reuseIdentifier) {
            self.configureCell(cell, forIndexPath: indexPath)

            if let dynamicCell = cell as? DynamicTableViewCell {
                let height = dynamicCell.heightInTableView(tableView)
                return height
            } else {
                // Fallback for non-DynamicTableViewCell cells
                let size = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
                let cellBoundsHeight = CGRectGetHeight(cell.bounds)
                if size.height > 0 && size.height >= cellBoundsHeight {
                    // +1 for the cell separator
                    return size.height + 1
                } else {
                    // In some situations (such as the content view not having any/enough constraints to get a height), the
                    // size from the systemLayoutSizeFittingSize: will be 0. However, because this can _sometimes_ be intended
                    // (e.g., when adding to a default style; see: DynamicSubtitleTableViewCell), we just return
                    // the height of the cell as-is. This may make some cells look wrong, but overall will also prevent 0 being returned,
                    // hopefully stopping some things from breaking.
                    return cellBoundsHeight + 1
                }
            }
        }
    }

    return UITableViewAutomaticDimension
}

private func cellForReuseIdentifier(reuseIdentifier: String) -> UITableViewCell? {
    if self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier] == nil {
        if let cellClass = self.cachedClassesForCellReuseIdentifiers[reuseIdentifier] {
            let cell = cellClass()

            self.offScreenWindow.addSubview(cell)
            self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier] = cell
        } else if let cellNib = self.cachedNibsForCellReuseIdentifiers[reuseIdentifier] {
            if let cell = cellNib.instantiateWithOwner(nil, options: nil).first as? UITableViewCell {
                self.offScreenWindow.addSubview(cell)
                self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier] = cell
            }
        }
    }
    return self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier]
}

DynamicTableViewCell

public func heightInTableView(tableView: UITableView) -> CGFloat {
    var height: CGFloat!
    if self.calculateHeight {
        self.setNeedsUpdateConstraints()
        self.updateConstraintsIfNeeded()

        self.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(self.bounds))

        self.setNeedsLayout()
        self.layoutIfNeeded()

        let size = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
        let boundsHeight = CGRectGetHeight(self.bounds)

        if size.height > 0 && size.height >= boundsHeight {
            // +1 for the cell separator
            height = size.height + 1
        } else {
            // In some situations (such as the content view not having any/enough constraints to get a height), the
            // size from the systemLayoutSizeFittingSize: will be 0. However, because this can _sometimes_ be intended
            // (e.g., when adding to a default style; see: DynamicSubtitleTableViewCell), we just return
            // the height of the cell as-is. This may make some cells look wrong, but overall will also prevent 0 being returned,
            // hopefully stopping some things from breaking.
            height = boundsHeight + 1
        }
    } else {
        height = self.cellHeight
    }

    if height < self.minimumHeight && self.minimumHeight != nil {
        return self.minimumHeight!
    } else {
        return height
    }
}

This solutions works for iOS 7 and 8, and so should any future solutions. This restriction has also removed the use of UITraitCollection, so I've not gone down that route (I'm not even sure it'd help)

Community
  • 1
  • 1
Joseph Duffy
  • 4,566
  • 9
  • 38
  • 68

1 Answers1

0

With IOS 8 and Autolayout You can set an estimated row height and set constraints for inner views with content view so it will scale according to the content in it. Here's my answer Which is the best approach among Autolayout or calculating the height using NSAttributedString, to implement dynamic height of an UITableViewCell?

Update: for IOS7 you need to calculate height when it is about to arrive you can do it in - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath Delegate method

#define HEIGHT_FOR_ONE_LINE 15; //define it according to your fonts
#define HEIGHT_FOR_SINGLE_ROWCELL 32;
#pragma mark - Table view data source
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *textToView= [self.yourDataSourceArray objectAtIndex:indexPath.row];
    CGFloat screenWidth=[[UIScreen mainScreen]bounds].size.width;
    CGFloat characterPerPoint= 10;
    CGFloat widthTaken= characterPerPoint * textToView.length;
    NSInteger numberOfLines= ceil(widthTaken/screenWidth);
    CGFloat height=numberOfLines* HEIGHT_FOR_ONE_LINE;
    height = height + HEIGHT_FOR_SINGLE_ROWCELL;

    return height;
}
Community
  • 1
  • 1
Asadullah Ali
  • 1,056
  • 14
  • 31
  • My implementation used to use iOS 8's automatic height calculation for cells but I've had a couple of issues with it, and I need this to work with iOS 7, too, so using and iOS 8-only solution is not possible – Joseph Duffy Apr 15 '15 at 12:13