Question

Setup:

I have a horizontal paging UIScrollView set up with help of the LXPagingViews class (to easily add a 'peeping' UIScrollView). This is set up, so you can flick between different views in the UIScrollView. I have added a UIPinchGestureRecognizer to every view, so when the user pinches on a view, it displays a 'zooming' animation, and segues into a detail view. A screenshot for more clarity:

My paging UIScrollView

Problem:

When I pinch one of the views, the zoom animation starts and the view segues into the detail view. This works fine, EXCEPT for the last view in the UIScrollView. In the last view (doesn't matter how many views it has, as long as it has more than 2), when the animation plays, it spawns a new view behind the view I tapped, and animates that one. This makes for a weird animation. Also, the view doesn't get blocked from input by the animation, because it spawns a new one. This means that the user can spawn multiple views quickly by pinching fast.

I hope this screenshot illustrates the problem a bit:

When the user pinches, a new view spawns behind and animates

What I have tried:

  • Debugging the gesture recognizer's superview subviews, to see if more views spawn
  • Monitoring what view get's animated. For every pinch the user does, another view gets animated for some reason
  • Tried to use another way of setting the image instead of lazy-loading. Didn't help
  • Tried other images, more and less views, orientation differences and empty views.

Code:

Adding the gesture recognizer to the view:

- (UIView<ReusableView> *)pagingView:(PagingView *)thePagingView reusableViewForPageIndex:(NSUInteger)thePageIndex withFrame:(CGRect)theFrame {
    // View's Identifier
    static NSString *theIdentifier = @"voucherDetailView";
    VoucherDetailView *thePageView = (VoucherDetailView *)[thePagingView dequeueReusableViewWithIdentifier:theIdentifier];
    if (thePageView == nil) {
        thePageView = [[VoucherDetailView alloc] initWithFrame:theFrame];
    } else {
        thePageView.frame = theFrame;
    }

    Voucher *voucher = [_vouchers objectAtIndex:thePageIndex];
    NSURL *fullVoucherImageUrl = [NSURL URLWithString:voucher.fullVoucherImage];
    [thePageView.voucher setImageWithURL:fullVoucherImageUrl placeholderImage:[UIImage imageNamed:@"11283253-nederland_kaart"]];

    // Gesture recognizer    
    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(openZoomablePinch:)];
    [thePageView addGestureRecognizer:pinchGesture];

    return thePageView;
}

The pinch handler:

- (void)openZoomablePinch:(UIPinchGestureRecognizer*)sender {
    // Only allow one pinch to activate the animation
    if (sender.state == UIGestureRecognizerStateBegan) {
        // Bring the view to the front so it doesn't clip with the peeping views
        [sender.view.superview bringSubviewToFront:sender.view];
        CGAffineTransform trans = sender.view.transform;
        [UIView animateWithDuration:0.5 delay:0.0 options:UIViewAnimationOptionCurveEaseOut
                         animations:^{
                             sender.view.transform = CGAffineTransformScale(sender.view.transform, 1.15, 1.15);
                         }
                         completion:^(BOOL finished) {
                             if (finished && self.view.window) {
                                 sender.view.transform = trans;
                                 [self performSegueWithIdentifier:@"voucherZoomSegue" sender:self];
                             }
                         }];
    }
}

EDIT: PagingView code that gets called on the last view when pinching:

    UIView<ReusableView> *theRightMostReusableView = [self.visibleReusableViews lastObject];
    NSUInteger theRightMostPageIndex = (NSUInteger)floorf(CGRectGetMinX(theRightMostReusableView.frame) / thePageWidth);
    while ((theRightMostPageIndex != MAX(0, self.numberOfItems - 1)) && (theRightMostPageIndex < theToIndex)) {
        theRightMostPageIndex = MIN(theRightMostPageIndex + 1, MAX(0, self.numberOfItems - 1));
        CGFloat theMinX = theRightMostPageIndex * thePageWidth;
        CGRect theRect = CGRectMake(theMinX, 0.0f, thePageWidth, thePageHeight);
        UIView<ReusableView> *theReusableView = [self.dataSource pagingView:self reusableViewForPageIndex:theRightMostPageIndex withFrame:theRect];
        if (!CGRectContainsRect(theRect, theReusableView.frame)) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                           reason:[NSString stringWithFormat:
                                                   @"theReusableView's frame (%@) must be contained by the given frame (%@)",
                                                   NSStringFromCGRect(theReusableView.frame),
                                                   NSStringFromCGRect(theRect)]
                                         userInfo:nil];
        }
        [self.visibleReusableViews addObject:theReusableView];
        [self addSubview:theReusableView];
    }

EDIT 2:

I have uploaded a small demo project here. The way to use it, set the simulator (or your device) in landscape, go to Peeping Paging View, and try to pinch/tap the views (except the first one). As you can see, the views get zoomed, except for the last view, which spawns a new view behind it and zooms that one (the '9' stays the same size).

The code is in the PeepingPagingViewController.m.

Solution (thanks to Oladya Kane):

The rightmost visible index is mistakingly indexed as the leftmost visible index. The code I had to change was the line:

NSUInteger theRightMostPageIndex = (NSUInteger)floorf(CGRectGetMinX(theRightMostReusableView.frame) / thePageWidth);

to

NSUInteger theRightMostPageIndex = MIN((NSInteger)floorf((CGRectGetMaxX(theRightMostReusableView.frame) - 0.1f) / thePageWidth), MAX(0, self.numberOfItems - 1));
Was it helpful?

Solution

I've looked through you demo code and found following bug:

When your pinch gesture recognizer is called, you animate it's sender frame. This forces PageView to layout its subviews -> it calls your delegate method

- (UIView<ReusableView> *)pagingView:(PagingView *)thePagingView reusableViewForPageIndex:(NSUInteger)thePageIndex withFrame:(CGRect)theFrame

Here you provide view to PagingView and it adds your view beneath your last view, messing up your view hierarchy. I think you might use some kind of a flag somewhere to prevent PagingView from requesting new views. The strangest thing is that this kind of behavior happens only for last view. Hope this will help you.

EDIT:

I found the problem. In the method

- (void)layoutSubviewsFromIndex:(NSUInteger)theFromIndex toIndex:(NSUInteger)theToIndex

indexes of the leftmost and rightmost visible views are computed. But the rightmost visible index is mistakenly computed as the leftmost index, so, you need to replace this line:

NSUInteger theRightMostPageIndex = (NSUInteger)floorf(CGRectGetMinX(theRightMostReusableView.frame) / thePageWidth);

With this one:

NSUInteger theRightMostPageIndex = MIN((NSInteger)floorf((CGRectGetMaxX(theRightMostReusableView.frame) - 0.1f) / thePageWidth), MAX(0, self.numberOfItems - 1));

This will prevent PagingView from requesting new views for already visible last (rightmost) view.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top