20

I got a rather complicated collectionView cell and I've noticed that if I scroll really fast on my collectionView it crashes the app.

One of the error I got is this:

negative or zero sizes are not supported in the flow layout

I noticed if I just return a float value e.g. 500, in my UICollectionView sizeForItemAtIndexPath: method, it doesn't crash.

My collection view has dynamic cell heights.

I'm parsing HTML Attributed string in my sizeForItemAtIndexPath: method using this library:

https://github.com/mmislam101/HTMLAttributedString

Anyone know what causes the above error message to occur in particular?

Update

The other error I also see in my Bugsense report related to this crash is:

-[__NSArrayM objectAtIndex:]: index 2 beyond bounds [0 .. 1]

This happens when I'm scrolling too fast = /

Update 2

Bugsense stacktrace shows the crash order method calls is:

1) collectionView:layout:sizeForItemAtIndexPath:

2) calculateFeedCellHeightForIndexPath: (this is one of my own method, not Apple's)

3) dynamicHeightForHTMLAttributedString:UsingWidth:AndFont: (this is one of my own method, not Apple's)

4) HTMLAttributedString attributedStringWithHtml:andBodyFont: line 35

5) HTMLAttributedString attributedString line 79

6) scrollViewDidScroll: line 174

7) setFeedFooterHeight:animated: line 124

My setFeedFooterHeight:animated: method is:

-(void)setFeedFooterHeight:(CGFloat)newHeight animated:(BOOL)animated
{
    footerHeightConstraint.constant = newHeight;

    if(animated)
    {
        [UIView animateWithDuration:0.35 animations:^{
            [self layoutIfNeeded];  // <------ crash on this line it seems
        }];
    }
    else
    {
        [self.feedFooterView layoutIfNeeded];
    }
}

However, when I run the app straight from Xcode rather than Testflight as above, Xcode stops at step 5 above, which yields this piece of code:

- (NSAttributedString *)attributedString
{
    __block NSString *css       = @"<style>";

    [_cssAttributes enumerateObjectsUsingBlock:^(NSString *cssAttribute, NSUInteger idx, BOOL *stop) {
        css = [css stringByAppendingString:cssAttribute];
    }];

    css                         = [css stringByAppendingString:@"</style>"];
    NSString *htmlBody          = [_html stringByAppendingString:css];

    NSStringEncoding encoding   = NSUnicodeStringEncoding;
    NSData *data                = [htmlBody dataUsingEncoding:encoding];

    NSDictionary *options       = @{NSDocumentTypeDocumentAttribute         : NSHTMLTextDocumentType,
                                    NSCharacterEncodingDocumentAttribute    : @(encoding)};

    // app crashes here on this next line.
    NSAttributedString *body    = [[NSAttributedString alloc] initWithData:data
                                                                options:options
                                                     documentAttributes:nil
                                                                  error:nil];

    return body;
}

I read on other threads something about wrapping uicollectionview reload until uicollection.isTracking becomes false?

I tried that but didn't seem to help.

Update 3

OK, I accidentally stumble upon the cause of that error.

It's related to the [collectionView.collectionFlowLayout invalidateLayout]; call.

I added a 1.0 second delay and the problem appears to be gone.

Zhang
  • 11,549
  • 7
  • 57
  • 87
  • So is your code ever returning a zero / negative height? When? – Wain Oct 08 '14 at 06:36
  • I tried to do a if(returnedHeight == 0) { NSLog(@"height is 0"; } before my return statement for sizeForItemAtIndexPath, it never reach that code breakpoint. – Zhang Oct 08 '14 at 06:37
  • Don't check for equality with zero, check for `< 1` – Wain Oct 08 '14 at 06:42
  • Share your code in dropbox, so we can know more detail? – gabbler Oct 08 '14 at 07:55
  • T-T I wish I could but it's sensitive information that the company doesn't want to disclose. Looks like I'm on my own. I appreciate the help nonetheless. – Zhang Oct 08 '14 at 08:10

4 Answers4

5

I think this is an iOS 8 bug related to UICollectionView's delegate/datasource methods, and / or NSAttributedString.

I can get this same crash in one project by creating an NSAttributedString from HTML in sizeForItemAtIndexPath: If I create an attributed string first in numberOfRows: this stops the crash: so likely issue is something in UIKit not being initialised properly, or a buffer is being shared between the HTML parser and one of the UICollectionView sizing methods.

Note: on iOS 7.1 I get a different exception: "UICollectionView recieved layout attributes for a cell with an index path that does not exist:"


Try this experiment: call something like:

NSData *data = [@"<a href=\"http://www.google.com\">link</a>" dataUsingEncoding:NSUnicodeStringEncoding];
NSAttributedString *s = [[NSAttributedString alloc] initWithData:data options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}  documentAttributes:nil error:nil];

in, say:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

This seems to force something (a parser perhaps?) to be initialised, such that when I then use similar code in:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

it doesn't crash. Crazy town.

I'm going to download 8.1 beta 2 and test there, and will report back.

Philip McDermott
  • 1,068
  • 1
  • 10
  • 9
  • Hey Philip, thanks for your input. Sounds like the scenario you describe matches mine. Do it first in the numberOfRows method you say? I'm a bit reluctant, that would mean I am doing double the amount of work, i.e. the scrolling would be even slower than it already is, no? or do you mean just force the parser by creating any random NSAttributedString? I also try to avoid caching because it's a pain in the but. My page have deletable rows and update-able rows such as aysnchronous image loaded from a URL. I will try your suggesting when I get back to the office next week. Thanks. – Zhang Oct 11 '14 at 03:26
  • Yes, just do one arbitrary string to force the parser. I'm considering caching anyway (since you may do the same work calculating heights and in cells), but actually I might go full TextKit instead of UITextView - I"m finding to many niggly bugs for it to be stable. – Philip McDermott Oct 13 '14 at 16:44
  • Still happens even with the additional code in the numberOfSections. By the way, if the above code works for you, don't you just need to do it once in the viewDidLoad() method instead of in numberOfItems() ? – Zhang Oct 14 '14 at 03:23
  • Yeah viewDidLoad should work: I just wanted to know that the collectionView would have been instantiated already. Odd that it doesn't help in your case: perhaps try later on? – Philip McDermott Oct 28 '14 at 10:25
3

make sure in your code where you define collectionViewLayout.itemSize that size does not contain negative/zero values

2

Use this:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    if (width <= 0 || height <= 0) {
        return CGSize(width: 0, height: 0)
    }
}
OmG
  • 18,337
  • 10
  • 57
  • 90
CrazyPro007
  • 1,006
  • 9
  • 15
1

My app was crash as I was sending width is -6 to sizeForItemAt due to some wrong calculation logic

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: width, height: height)
}

width & height should always be non-negative numbers (>= 0)

Keshav
  • 2,965
  • 3
  • 25
  • 30