How to fix scrolling to a visible cell in UICollectionView with custom layout?

Andrey Chevozerov picture Andrey Chevozerov · Dec 26, 2014 · Viewed 7k times · Source

I'm working on custom value picker inspired by UIPickerView. It looks like that: Technical prototype screen 1

As you can see, one of the main features of this picker is the central cell the should be wider than others to make it's neighbours visible beside of the central frame. When you scroll the picker with a pan gesture it should dynamically change central value and adjust cells according to the logic above. And it works just perfect: Technical prototype screen 2

The problem is in the tap gesture. When the user select any item on the picker by performing tap on it the picker trying to scroll to that item. But since it's offset was changed by the custom layout UIScrollView scrolls to the wrong point. And it looks like that: Technical prototype screen 3

When I trying to scroll to the offscreen cell all things works fine - that cell was not affected by the layout and it's coordinates are correct. The issue rises only for visible cells. I'm completely out of any ideas of how to fix that. You could find the whole project here: Carousel Collection View Test Project

Please find some significant code below.

// CarouselLayout.m

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *array = [super layoutAttributesForElementsInRect:rect];
    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    CGRect center = CGRectMake(CGRectGetMidX(visibleRect) - 1.0, 0.0, 2.0, CGRectGetHeight(visibleRect));
    CGFloat coreWidth = CGRectGetWidth(self.centralFrame) / 3.0;

    for (UICollectionViewLayoutAttributes *attributes in array) {
        if (CGRectIntersectsRect(attributes.frame, rect)){
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
            CGFloat offset = 0.0;
            CGRect coreFrame = CGRectMake(attributes.center.x - (coreWidth / 2.0), 0.0, coreWidth, CGRectGetHeight(self.centralFrame));
            if (CGRectIntersectsRect(center, coreFrame)) {
                if (attributes.indexPath.item % 2 == 0) {
                    self.centralItemOffset = (CGRectGetWidth(self.centralFrame) - CGRectGetWidth(attributes.frame) - 4.0) / 2.0;
                    if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:didChangeCentralItem:)]) {
                        [(id <CarouselLayoutDelegate>)self.collectionView.delegate collectionView:self.collectionView layout:self didChangeCentralItem:attributes.indexPath];
                    }
                }
            }
            offset = (distance > 0) ? -self.centralItemOffset : self.centralItemOffset;
            attributes.center = CGPointMake(attributes.center.x + offset, attributes.center.y);
        }
    }
    return array;
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
    CGRect targetRectHorizontal = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray *array = [super layoutAttributesForElementsInRect:targetRectHorizontal];
    for (UICollectionViewLayoutAttributes *attributes in array) {
        if (attributes.indexPath.item % 2 == 1) {
            continue;
        }
        CGFloat itemHorizontalCenter = attributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

// ViewController.m

- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.item % 2 == 1) {
        return;
    }
    NSString *nextValue = [self valueAtIndexPath:indexPath];
    [self scrollToValue:nextValue animated:YES];
    self.currentValue = nextValue;
}

- (void)scrollToValue:(NSString *)value animated:(BOOL)animated {
    NSIndexPath *targetPath = nil;
    NSIndexPath *currentPath = nil;
    for (NSString *item in self.itemsArray) {
        if (!targetPath && [value isEqualToString:item]) {
            targetPath = [NSIndexPath indexPathForItem:([self.itemsArray indexOfObject:item] * 2) inSection:0];
        }
        if (!currentPath && [self.currentValue isEqualToString:item]) {
            currentPath = [NSIndexPath indexPathForItem:([self.itemsArray indexOfObject:item] * 2) inSection:0];
        }
        if (targetPath && currentPath) {
            break;
        }
    }
    if (targetPath && currentPath) {
        [self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
    }
}

If it's not enough please ask for additional code in comments.

Answer

gvuksic picture gvuksic · Jan 4, 2015

In scrollToValue method change:

if (targetPath && currentPath) {
    [self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}

into:

if (targetPath && currentPath) {
    if (targetPath.row < currentPath.row) {
        [self.itemsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.itemsArray.count inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
        [self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
    } else {
        [self.itemsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
        [self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];

    }
}

and it'll work.