19

The same behavior of UICollectionView as described here has been led to this question. Even though I decided to post my own one, because I did further investigations, I didn't want to post in a comment or in edit of the question mentioned above.

What happens?:

When large cells being displayed in a UICollectionView with a UICollectionViewFlowLayout, after scrolling the collection view to a certain offset, the cells will disappear.

When scrolling further until another cell comes into visible area, the vanished/hidden cell becomes visible again.

I tested with a vertical scrolling collection view and full-width-cells, but I'm rather sure, that it would also happen with similar setups for horizontal scrolling.

What are large cells?:

The described behavior happens with cells higher than twice the display height (960.f + 1.f on 3,5 inch displays, 1136.f + 1.f on 4 inch).

What exactly happens?:

When the scrolling offset of the collection view exceeds cell.frame.origin.y + displayHeightOfHardware the cells hidden property is set to YES and -collectionView:didEndDisplayingCell:forItemAtIndexPath: gets called (e.g. the first cell changes to hidden when scrollingOffset.y reaches 481.f on 3,5-inch-iPhone).

As described above, when scrolling until next cell comes into view, the hidden cell gets displayed again (i.e. hidden property changes to NO) and furthermore, when scrolling far enough the cell will never vanish again, when it shouldn't, no matter where you scroll to.

This changes when working with cells larger than triple-display-height (1441.f/1705.f). Those show the same behavior, but it stays the same, no matter how far they're being scrolled up and down.

What else?:

The situation can not be fixed by overriding -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds to return YES.

The cells cannot being forced to display with setting the hidden property to NO programmatically after they were hidden (in didEndDisplayingCell for example)

So, whats the question?:

I'm pretty sure, that this is a bug in UICollectionView/Controller/Cell/Layout and I'll submit a TSI at Apple. But for the meantime: Has anyone any ideas for a quick hack solution?

Community
  • 1
  • 1
Kai Huppmann
  • 10,705
  • 6
  • 47
  • 78

4 Answers4

9

i have a VERY dirty and internal solution for this problem:

@interface UICollectionView ()
- (CGRect)_visibleBounds;
@end

@interface MyCollectionView : UICollectionView

@end

@implementation MyCollectionView

- (CGRect)_visibleBounds {
    CGRect rect = [super _visibleBounds];
    rect.size.height = [self heightOfLargestVisibleCell];
    return rect;
}

- (float)heightOfLargestVisibleCell {
    // do your calculations for current max cellHeight and return it 
    return 1234;
}

@end
Jonathan Cichon
  • 4,396
  • 16
  • 19
  • When you say 'internal solution', do you mean a solution that will not get through the app store process? – Jack Cox Jan 10 '13 at 14:53
  • @JackCox `_visibleBounds` is privat-API, so probably not. Most times such hacks remain undetected by apple but nothing guaranteed. – Jonathan Cichon Jan 10 '13 at 15:00
  • Thanks. That's what I figured. – Jack Cox Jan 10 '13 at 15:15
  • It works! Since - as you said - this one will probably rejected in Apple's approval process, I'll wait for them (TSI already submitted) to listen to what they have to say. – Kai Huppmann Jan 10 '13 at 15:46
  • Fantastic fix btw! You might wanna set heightOfLargestVisibleCell to be the max between the heightOfLargestVisibleCell and the screen height for example for cases where heightOfLargestVisibleCell is small =) Not doing so caused parts of my UICollectionViewCell to not show. – Abdo Jan 16 '13 at 19:20
  • 2
    Con someone post a link to the radar so I can dup it? I'd like to see this resolved. – kvn Feb 07 '13 at 16:13
  • My _visibleBounds never gets called. Also causes a compiler error the way you did it. Can you clarify what is required to try this? – Cameron Lowell Palmer Mar 30 '13 at 21:39
  • @CameronLowellPalmer did you add the Extension `@interface UICollectionView ()` in your implementation file? – Jonathan Cichon Apr 02 '13 at 06:57
  • @JonathanCichon yes I did and it complained about [super _visibleBounds]. – Cameron Lowell Palmer Apr 03 '13 at 08:58
  • @CameronLowellPalmer if xcode complains about [super _visibleBounds] is unknown your implementation is missing the `@interface UICollectionView () - (CGRect)_visibleBounds; @end` part. You have to tell the compiler that there is a method called `_visibleBounds` in the root class `UICollectionView` – Jonathan Cichon Apr 03 '13 at 11:53
  • This fix works for me also but I still have a little problem. If i perform a `reloadData' when my scroll position is in the middle of a large cell, this one disappear and I have to scroll to a certain offset to see it reappear. Do you know how could I avoid this behavior ? – Yaman Jul 15 '13 at 12:56
  • @Yaman can you post some code? I can not reproduce this behavior/ – Jonathan Cichon Jul 15 '13 at 18:01
  • @JonathanCichon I've posted a separated question with my code here : http://stackoverflow.com/questions/17657530/large-uicollectionviewcell-disappear-on-scrolling – Yaman Jul 15 '13 at 22:06
  • seems like the problem is fixed with iOS7GM – Jonathan Cichon Sep 20 '13 at 14:47
3

I have a workaround that seems to be working for me and should not run amok of Apple's rules for iOS applications.

The key is the observation that the large cells bounds are the issue. I've worked around that by ensuring that one edge of the cell is within the viewable area of the scrollable content region. You'll obviously need to subclass the UICollectionViewFlowLayout class or UICollectionViewLayout depending on your needs and make use of the contentOffset value to track where you are in the UIScrollView.

I also had to ensure:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 

returns YES or face a runtime exception indicating the layout was invalid. I keep the edge of the larger cell bound to the left edge in my case. This way you can avoid the erroneous bounds intersection detection for these larger cells.

This does create more work depending on how you would like the contents of the cell to be rendered as the width/height of the cell is being updated as you scroll. In my case, the subviews within the cell are relatively simple and do not require a lot of fiddling with.

As requested here is an example of my layoutAttributesInRect

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    NSArray *vertical = myVerticalCellsStore.cells;
    NSInteger startRow = floor(rect.origin.y * (vertical.count)/ (vertical.count * verticalViewHeight + verticalViewSpacing * 2));
    startRow = (startRow < 0) ? 0 : startRow;

    for (NSInteger i = startRow; i < vertical.count && (rect.origin.y + rect.size.height >= i * verticalViewHeight); i++) {
        NSArray *horizontals = myHorizontalStore.horizontalCells;
        UICollectionViewLayoutAttributes *verticalAttr = [self layoutAttributesForSupplementaryViewOfKind:@"vertical" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
        if (CGRectIntersectsRect(verticalAttr.frame, rect)) {
            [attributes addObject:verticalAttr];
        }

        BOOL foundAnElement = NO;
        for (NSInteger j = 0 ; j < horizontals.count; j++) {
            MYViewLayoutAttributes *attr = (MyViewLayoutAttributes *)[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:j inSection:i]];
            if (CGRectIntersectsRect(rect, attr.frame)) {
                [attributes addObject: attr];
                foundAnElement = YES;
            }
            else if (foundAnElement) {
                break;
            }
        }
    }
    return attributes;
}

This is my sanitized code. Basically I calculate about were the first cell should be based on the cell height. In my case that is fixed, so the calculation is pretty easy. But my horizontal elements have various widths. So the inner loop is really about figuring out the right number of horizontal cells to include in the attributes array. There I'm using the CGRectIntersectsRect to determine if the cell intersects. Then the loop keeps going until the intersection fails. And if at least one horizontal cell has been found the loop will break. Hope that helps.

kvn
  • 1,295
  • 11
  • 28
  • You doing this frame adjustment on the layout attributes in layoutAttributesForElementsInRect or on the cells frame? If on the cell at what point? – nbransby Mar 05 '13 at 19:17
  • I do it in the layoutAttributesForElementsInRect implementation. – kvn Mar 08 '13 at 20:23
  • +1 for identifying that keeping the edges of the cell within the bounds of the collection view prevents this from happening. I'm working on having my cells layout their subviews to compensate for this, but it seems to be having a negative impact on performance. Bummer. – CharlieMezak Jun 03 '13 at 20:37
  • Can you provide an example of `layoutAttributesForElementsInRect` implementation which do the trick plz ? – Yaman Jul 09 '13 at 12:31
  • @Yaman I have updated the answer to include an example of the `layoutAttributesForElementsInRect` implementation I had to create. Hope that helps. – kvn Jul 18 '13 at 14:24
0

My solution is basically the same as Jonathan's but in a category, so you don't have to use your own subclass.

@implementation UICollectionView (MTDFixDisappearingCellBug)

+ (void)load {
    NSError *error = nil;
    NSString *visibleBoundsSelector = [NSString stringWithFormat:@"%@isib%@unds", @"_v",@"leBo"];

    if (![[self class] swizzleMethod:NSSelectorFromString(visibleBoundsSelector) withMethod:@selector(mtd_visibleBounds) error:&error]) {
        FKLogErrorVariables(error);
    }
}

- (CGRect)mtd_visibleBounds {
    CGRect bounds = [self mtd_visibleBounds]; // swizzled, no infinite loop
    MTDDiscussCollectionViewLayout *layout = [MTDDiscussCollectionViewLayout castedObjectOrNil:self.collectionViewLayout];

    // Don`t ask me why, but there's a visual glitch when the collection view is scrolled to the top and the max height is too big,
    // this fixes it
    if (bounds.origin.y <= 0.f) {
        return bounds;
    }

    bounds.size.height = MAX(bounds.size.height, layout.maxColumnHeight);

    return bounds;
}

@end
myell0w
  • 2,200
  • 2
  • 21
  • 25
  • but be aware that this can cause performance issues, since the visible bounds can get very big and UICollectionView needs to calculate the frame of each cell in the visible bounds. – myell0w May 23 '13 at 18:54
  • I would add a conditional to only swizzle that on iOS6. And for everyone wondering... 'visibleBounds' is considered private API and will get you rejected if you don't obfuscate the selector. – steipete May 23 '13 at 20:49
0

I found that this issue only occurred when using a subclassed UICollectionViewLayoutAttributes and when that attribute class did not have a correct isEqual: method.

So for example:

@implementation COGridCollectionViewLayoutAttributes
- (id)copyWithZone:(NSZone *)zone
{
    COGridCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
    attributes.isInEditMode = _isInEditMode;
    return attributes;
}

- (BOOL)isEqual:(id)other {
    if (other == self) {
        return YES;
    }
    if (!other || ![[other class] isEqual:[self class]]) {
        return NO;
    }
    if ([((COGridCollectionViewLayoutAttributes *) other) isInEditMode] != [self isInEditMode]) {
        return NO;
    }

    return [super isEqual:other];
}

@end

Worked but originally I had:

return YES;

This is on iOS 7.

AlexK
  • 638
  • 6
  • 12