SwiftKickMobile / TLLayoutTransitioning

Enhanced transitioning between UICollectionView layouts in iOS.
MIT License
355 stars 47 forks source link

UICollectionView not centering transition in iOS 9, worked fine in iOS 8. #26

Closed wanderingme closed 9 years ago

wanderingme commented 9 years ago

The app essentially transitions between two different UICollectionViewFlowLayouts in a UICollectionView. I've not had a single problem with the TLLayoutTransitioning class prior to iOS 9. Thanks so much for taking the time to make and share this, by the way! It's a life saver. :)

For some reason since updating to iOS 9 (app still compiled for 8+) the transition doesn't center the offset, and instead seems to throw the whole transition off now while transitioning from large to small layouts, as seen here:

ripewellwornkagu-size_restricted

Any thoughts what might've changed?

wanderingme commented 9 years ago

I'm also receiving this issue in the output window:

2015-09-21 13:04:54.231 [1504:472068] Logging only once for UICollectionViewFlowLayout cache mismatched frame 2015-09-21 13:04:54.233 [1504:472068] UICollectionViewFlowLayout has cached frame mismatch for index path <NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0} - cached value: {{73.099999999999994, 351}, {173.80000000000001, 220}}; expected value: {{81, 361}, {158, 200}} 2015-09-21 13:04:54.234 [1504:472068] This is likely occurring because the flow layout subclass NNPodCollectionViewFlowSmallLayout is modifying attributes returned by UICollectionViewFlowLayout without copying them

wtmoose commented 9 years ago

This is likely occurring because the flow layout subclass NNPodCollectionViewFlowSmallLayout is modifying attributes returned by UICollectionViewFlowLayout without copying them

Did you check whether NNPodCollectionViewFlowSmallLayout is internally modifying attributes without copying them? I don't think TLLayoutTransitioning modifies your layouts (I'll double check this when I get a chance to look deeper into this).

wanderingme commented 9 years ago

Yes, I was making a copy. Unless I'm completely missing something, I think that should do it, though the error still claims I'm not copying them. I assumed this was another issue with iOS 9 throwing warnings where it shouldn't, though this could very well be PEBKAC.

Here's the code. Both times the attributes array are established, I've created a copy. Thanks again for getting back to me.

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);

    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray* array = [[self layoutAttributesForElementsInRect:targetRect] copy];

    for (UICollectionViewLayoutAttributes* layoutAttributes in array) {
        if (layoutAttributes.representedElementCategory != UICollectionElementCategoryCell)
            continue; 

        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemHorizontalCenter - horizontalCenter;

            layoutAttributes.alpha = 0;
        }
    }
    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{

    NSArray *allAttributesInArray = [[super layoutAttributesForElementsInRect:rect] copy];

    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    for(UICollectionViewLayoutAttributes* attributes in allAttributesInArray){
        if(CGRectIntersectsRect(attributes.frame, rect)){
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
            CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
            if (ABS(distance) < ACTIVE_DISTANCE) {
                CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
                attributes.zIndex = round(zoom);
            }
        }
    }

    return allAttributesInArray;
}
wanderingme commented 9 years ago

I was able to find another repository experiencing a similar UICollectionViewFlowLayout error, and they worked out a solution that I merged into mine, and while this does get rid of the UICollectionViewFlowLayout errors, it doesn't fix the original issue with TLLayoutTransitioning centering the transition.

So we can rule this out as a cause, I think.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *allAttributesInArray = [NSMutableArray array];

    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    for(UICollectionViewLayoutAttributes* attributes in [super layoutAttributesForElementsInRect:rect]){

        UICollectionViewLayoutAttributes* attributesCopy = [attributes copy];

        if(CGRectIntersectsRect(attributes.frame, rect)){
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
            CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
            if (ABS(distance) < ACTIVE_DISTANCE) {
                CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
                attributesCopy.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
                attributesCopy.zIndex = round(zoom);
            }
        }

        [allAttributesInArray addObject:attributesCopy];
    }

    return allAttributesInArray;
}
wtmoose commented 9 years ago

Something doesn't look right in your animated image, but its hard for me to put my finger on it. Can you do the same example on iOS8 with correct behavior for comparison?

wanderingme commented 9 years ago

It should be doing this. Transitioning the center. Latest build on iOS 8.

livejoyfulgelding-size_restricted

wtmoose commented 9 years ago

Thanks. The first thing I would do, if you haven't already done so, is narrow down the issue by determining which of the following are wrong:

  1. The layout frames
  2. The collection view's contentOffset
  3. The contentOffset calculated by TLLayoutTransitioning

For (1), I suggest adding a logging statement in [TLTransitionLayout prepareLayout]. In the loop where the poses are calculated, select one of them and log the frame. Then during the transition, you can see how that frame transitions between layouts.

For (2), I suggest adding a logging statement in [TLTransitionLayout setTransitionProgress:time:] to log the collection view's current contentOffset (before being set to offset) and the offset variable.

For (3), just log the result of your call to toContentOffsetForLayout.

Hopefully, you'll find that one of the above is wrong and then we can go from there.

wanderingme commented 9 years ago

The problem definitely seems to be occurring in 1. Both 2 & 3 appear to be correct values.

Inside [TLTransitionLayout prepareLayout] the frames while animating from large to small cells always starts at frame = (0 0; 0 0); even though they should reasonably all have a size of 320x568 on my device, and progressively larger x positions as they are horizontally stacked inside a collection view. For example, cell 1 should be something like:frame = (0 0; 325 568);, cell 2: frame = (320 0; 325 568);, cell 3: frame = (640 0; 325 568);, etc.

But I see something like this: fromPose = {{0, 0}, {0, 0}} with a toPose = {{921, 361}, {158, 200}} for instance. But the fromPose here should be something akin to {{921, 0}, {320, 568}}.

As the transition progresses, of course, they do find their end frames correctly, but the starting properties are all zero values, hence the odd transitional animation we're seeing in the first gif I embedded above. So from what I can gather, when [UICollectionViewFlowLayout layoutAttributesForItemAtIndexPath] is called, it isn't returning the frame for some reason. I'm not overriding it in my sublass, either. :-/

I slowed down the closing transition considerably so it's easier to see what's happening. I'll keep digging!

tallscaryankolewatusi-size_restricted

wanderingme commented 9 years ago

Update.

I found the potential culprit. Inside [UICollectionView+TLTransitioning transitionToCollectionViewLayout:(UICollectionViewLayout *)layout duration:(NSTimeInterval)duration easing:(AHEasingFunction)easingFunction completion:(UICollectionViewLayoutInteractiveTransitionCompletion)completion].

Whenever the UICollectionViewTransitionLayout *transitionLayout class variable is instantiated, the frame goes from having a value, i.e. frame = (642 0; 320 568);, to being zeroed out.

I found I could return an empty class instance without instantiating it with a class method and the frame would retain its value, i.e.:

 UICollectionViewTransitionLayout *transitionLayout;
return transitionLayout;

But obviously that defeats the purpose. So bizarre. Only does this in iOS 9. And it only happens when I transition from the larger layout to the small layout, not the other way around. I'll keep digging.

wanderingme commented 9 years ago

Here's a fix, though I'm not sure if this is the best way. I reloaded the UICollectionView data before setting the UICollectionViewTransitionLayout.

[self reloadData];

UICollectionViewTransitionLayout *transitionLayout = [self startInteractiveTransitionToCollectionViewLayout...
wtmoose commented 9 years ago

Whenever the UICollectionViewTransitionLayout *transitionLayout class variable is instantiated, the frame goes from having a value, i.e. frame = (642 0; 320 568);, to being zeroed out.

When you save the frame is zeroed out, you're referring to the frame on the layout attributes of the "from" layout? Just want to make sure I'm clear on that.

wanderingme commented 9 years ago

Yes, the from layout.

wanderingme commented 9 years ago

Turns out [self reloadData]; in UICollectionView+TransitionLayout.m (mentioned above as a temporary solution) caused other problems, specifically when setting the CGPoint offset from toContentOffsetForLayout:indexPaths:placement. Though the supplied indexPath is correct, the returned offset is incorrect. But only for the first transition. For the second it corrects itself magically.

For instance, supplying the following indexPath <NSIndexPath: 0xc000000006e00016> {length = 2, path = 0 - 55} would return this offset for the first transition {16050, -0} and then the correct one upon the second attempt {17655, -0}. The incorrect offset isn't random, however, it's actually the first cell in a newly concatenated datasource. I'll explain...

I load the collection view's datasource in increments of 25 from my db. As the user scrolls to the end of the first 25 cells, I load the next 25 and concat to the datasource, making it 50 cells in length, reload the data, and repeat as needed. Pretty straight forward. The incorrect offset above {16050, -0} is actually the offset for the very first cell in the newly loaded data, cell 26 for instance (the first cell after the initial 25 cells). Though the indexPath supplied is for cell 32. I hope I'm explaining that clearly enough.

When I do not add [self reloadData]; in UICollectionView+TransitionLayout.m, however, the issue isn't happening, and the offsets are correct for all transitions, but then the original issue with the frames being zeroed out comes back again.

I'm absolutely confounded. I'll continue to dig into this.

wtmoose commented 9 years ago

Regarding the issue where the frames are zero, could you check whether the fromPose in this line in -[TLTransitionLayout prepareLayout] is getting set to nil?

UICollectionViewLayoutAttributes *fromPose = self.poses
    ? [self.poses objectForKey:key]
    : [self.currentLayout layoutAttributesForItemAtIndexPath:indexPath];

Then I'd start tracing back to narrow down where things go wrong. For example, I'd look in -[TLTransitionLayout initWithCurrentLayout:nextLayout:supplementaryKinds: to see if

a) The currentLayout is the same instance as the one being used by the collection view. b) Whether the layout attributes are nil or the frames are zero at this point.

wanderingme commented 9 years ago

fromPose isn't being set to nil, just the frame set to zero.

a) currentLayout matches the instance being used by the collectionView. b) Not nil ever. Sometimes frame is zero. Because there a two transitions (small --> large && large --> small) I'll show the frames during each:

Small --> Large currentLayout (small) correct frame ✓ nextLayout (large) incorrect zeroed frame ✖, though this transition is visually correct ✓.

Large --> Small currentLayout (large) correct frame ✓ nextLayout (small) correct frame ✓, though this transition is visually incorrect ✖.

Though, it's worth repeating, the frame of my large UICollectionViewFlowLayout instance is zeroed out once the TLTransitionLayout transition call is made during the Large --> Small transition as mentioned before. Prior to this, it retains its frame. It seems somewhere within [UICollectionView+TLTransitioning transitionToCollectionViewLayout:duration:easing:completion] my UICollectionViewFlowLayout is losing its frame? I'll keep digging. Thanks.

wtmoose commented 9 years ago

It seems somewhere within [UICollectionView+TLTransitioning transitionToCollectionViewLayout:duration:easing:completion] my UICollectionViewFlowLayout is losing its frame?

In that case, you should be able debug in that method and narrow down where the frames get lost.

wanderingme commented 9 years ago

I believe I found the problem. I was calling -(void)invalidateLayout on my collection view, though in my large layout class I was setting shouldInvalidateLayoutForBoundsChange to NO. I stopped invalidating the layout and this fixed the frame zeroing issue. But NOT this issue, though I found a fix to that too...

I was instantiating my large and small layout classes only once and reusing those pointers every time I called [ UICollectionView+TLTransitioning transitionToCollectionViewLayout:layout:duration:easing:completion:], and for some reason because of this the collectionViewContentSize of the large layout class was not properly updating after concatenating to the UICollectionView's datasource. Simple fix was to release and reinit just before:

largeLayout = nil;
largeLayout = [[CustomUICollectionViewFlowLargeLayout alloc] init];

That's it.

wtmoose commented 9 years ago

Good to know you figured it out! Can this be closed now?

wanderingme commented 9 years ago

Yes. Thanks for the help.

User2004 commented 5 years ago

Click here for set collectionview flow layout