Scaling a UIView in response to changes in UIScrollView zoom

Jul 7, 2012 20:11 · 462 words · 3 minute read CALayer iOS UIView

The app that I’m currently working on is based around a series of map images that can be scrolled and zoomed. Rather than using MapKit or anything similar, they’re images that sit inside a UIScrollView - the map images aren’t geographically-accurate, so it was easier to use images than try to figure out the distortions needed to map geography to map.

There’s a series of “pins” that get placed onto the map as it loads - but because the pins were being added to the view that gets zoomed, the pin images themselves were getting scaled up as the map zoomed it. This looked nasty, and isn’t how the MapKit pins work - they stay a constant size relative to the screen itself regardless of the zoom scale of the view they are subviews of.

After scratching my head for a while, this is what I came up with - it’s a method that’s called by UIScrollView's scrollViewDidZoom: delegate method -

-(void)rescaleItemMarkers {

    float initialMapScale = 0.25;
    float finalMapScale = self.scrollView.zoomScale;

    // Clamp final map scales
    if (finalMapScale < 0.25) {
        finalMapScale = 0.25;
    } else if (finalMapScale > 1.0){
        finalMapScale = 1.0;
    }

    float scalingFactor = finalMapScale / initialMapScale;

    float pinCorrectionFactor = 1 / scalingFactor;

    CAMediaTimingFunction *easingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

    CABasicAnimation *xScaleAnimation;
    xScaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.x"];
    xScaleAnimation.timingFunction = easingCurve;
    xScaleAnimation.duration=0.3;
    xScaleAnimation.repeatCount=0;
    xScaleAnimation.autoreverses=NO;
    xScaleAnimation.removedOnCompletion = NO;
    xScaleAnimation.fillMode = kCAFillModeForwards;
    xScaleAnimation.fromValue = [NSNumber numberWithFloat:self.currentPinZoomFactor];
    xScaleAnimation.toValue=[NSNumber numberWithFloat:pinCorrectionFactor];

    CABasicAnimation *yScaleAnimation;
    yScaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.y"];
    yScaleAnimation.timingFunction = easingCurve;
    yScaleAnimation.duration=0.3;
    yScaleAnimation.repeatCount=0;
    yScaleAnimation.autoreverses=NO;
    yScaleAnimation.removedOnCompletion = NO;
    yScaleAnimation.fillMode = kCAFillModeForwards;
    yScaleAnimation.fromValue = [NSNumber numberWithFloat:self.currentPinZoomFactor];
    yScaleAnimation.toValue=[NSNumber numberWithFloat:pinCorrectionFactor];

    for (UIView *theView in self.itemViews) {

        CALayer *layer = theView.layer;
        [layer addAnimation:xScaleAnimation forKey:@"animateScaleX"];
        [layer addAnimation:yScaleAnimation forKey:@"animateScaleY"];

    }

    self.currentPinZoomFactor = pinCorrectionFactor;

}

The initialMapScale is the zoom scale that’s applied to the map as it’s loaded (in this case, the maps are 14 size at load). The finalMapScale is whatever scale the map ends up with at the end of the zoom. The pin images will be multiplied by this zoom scale, so this method calculates the inverse and applies a scaling transform to each pin to take it back down to the size that it started at.

Lines 6-11 are a bit of a hack to prevent the pins reacting to zoom factors below 0.25 - it’s possible to pinch in the scroll view below the minimum - if you don’t correct for this, the pins appear to “bounce” in a weird way when this happens.

The pins themselves are an array of UIViews, so lines 41 to 47 iterate through that array and apply the scale to each pin.

The transform is a simple scale factor - it’s worth noting lines 25 and 26, which are what you need to stop the change being immediately reverted as soon as the animation finishes.