Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resize UITableViewCell asynchronously vs. generated UIView-Encapsulated-Layout-Height

I am trying to resize a table view cell "from the inside", using iOS 8, thus not implementing

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

but using:

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 200;

The cell has constraints top to bottom.

The cell contains (among other things) a UIWebview that knows its size asynchronously after load by asking the scrollview for its height:

CGFloat fittingHeight = self.messageTextWebView.scrollView.contentSize.height;

(this is a variant where the webview is contained within another view and that view resizes accordingly).

Because the whole auto layout process is finished when this happens, I set

[self setNeedsUpdateConstraints];

on the view in the cell which bubbles up to the cell and down again with layoutSubviews.

In the end, I get the dreaded Unable to simultaneously satisfy constraints.. The constraint that breaks everything is

"<NSLayoutConstraint:0x7f8c29d157f0 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7f8c2b063eb0(270)]>"

This constraint is created by the table view when the cell is being calculated for the first time.

It kills my own:

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fa8a8515b70 V:[UIView:0x7fa8aa0263e0(263)]>

Although I set my constraints' priorities to 1000 as well as compression resistance to 1000, I cannot override the constraint created by the table view.

What's frustrating: If I replace the web view with a simple multiline label and know all the sizes during the first cell layout, everything works perfectly. It's the asynchronous nature of the web view's loading system that forces me to change the layout after the cell is rendered for the first time.

Now my question:

Is there any way around this? I don't really want to reload the cell. If the cell is the only one on screen, it's not being reused and the whole thing starts over again. Is this even possible or does the UITableView architecture depend on that outer constraint to render itself correctly?

Just for the fun of it, I set

self.translatesAutoresizingMaskIntoConstraints = NO;

on the cell itself, resulting in a cell that has a size of 0,0 and the constraint:

"<NSLayoutConstraint:0x7f88506663a0 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7f8850648400(0)]>"

but it's still there...

thanks for your help.

BTW: I already read everything there is regarding the matter, including: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

like image 235
k1th Avatar asked Nov 16 '14 14:11

k1th


2 Answers

Here's a UITableViewCell subclass that manages a UIWebView and adjusts its height correctly for a UITableView set to use UITableViewAutomaticDimension.

Mainly, maintain a NSLayoutConstraint for the UIWebView height. Update its constant in updateConstraints. Tell the parent tableview to recalc cell heights when there are changes.

@implementation WebTableViewCell
{
    IBOutlet UIWebView*             _webview; // glued to cell content view using edge constraints

    IBOutlet NSLayoutConstraint*    _heightConstraint; // height constraint for the webview itself
}

- (void) awakeFromNib
{
    _webview.scrollView.scrollEnabled = NO;

    // use this to test various document heights
    //  [_webview loadHTMLString: @"<html><body style='height:100px;'>Hello, World</body></html>" baseURL: nil];

    // or, a real web page
    [_webview loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: @"http://stackoverflow.com/users/291788/tomswift"]]];
}

- (void) updateConstraints
{
    [super updateConstraints];

    // get the document height.
    CGFloat height = [[_webview stringByEvaluatingJavaScriptFromString: @"document.height"] floatValue];
    if ( _heightConstraint.constant != height )
    {
        // update
        _heightConstraint.constant = height;

        // ask the tableview to recalc heights
        dispatch_async(dispatch_get_main_queue(), ^{

            UITableView* tableView = (UITableView*)self.superview;
            while ( tableView != nil && ![tableView isKindOfClass: [UITableView class]] )
                tableView = (UITableView*)tableView.superview;


            [tableView beginUpdates];
            [tableView endUpdates];
        });
    }
}

- (void) webViewDidFinishLoad:(UIWebView *)webView
{
    [self setNeedsUpdateConstraints];
}

@end
like image 65
TomSwift Avatar answered Nov 15 '22 20:11

TomSwift


After some more research, my conclusion is:

Either use attributed strings that now allow HTML content (since iOS7):

[[NSAttributedString alloc] initWithData:[message dataUsingEncoding:NSUTF8StringEncoding]
options:
@{
     NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
     NSCharacterEncodingDocumentAttribute: [NSNumber numberWithInt:NSUTF8StringEncoding] 
} 
documentAttributes:nil error:&err];

The HTML parser is ok, limitations are

  • you should never use src tags referencing content on the network. Content is loaded on the main thread. I figure they had to do that to force synchronous rendering.
  • there is no interaction (which is actually a good thing).

Or: A collection view might work but I haven't converted the table view yet - depending on my test results with NSAttributedString I might skip that...

Update

I tried using the fantastic https://github.com/TTTAttributedLabel/TTTAttributedLabel It renders attributed strings with the added bonus of detecting links. Which was everything I needed – almost. It doesn't render tags...

Got back to UITextView which surprisingly works just fine.

like image 31
k1th Avatar answered Nov 15 '22 21:11

k1th