We wrote Titanium in order to respond to the following user story:
As an EasyShift user, I want all images inside of a job / survey to be tappable, zoomable, etc., so that I can get a better look at them.
Strictly speaking, we could have dealt with this problem by using a good old UIScrollView and a standard modal transition. However, we felt like this deserved a little bit more love.
Apple provides an easy way to customise transitions between view controllers: the UIViewControllerAnimatedTransitioning
protocol. It lets you manually specify all the animations for presenting and dismissing view controllers.
In this case, I wanted to recreate an animation like the one in the Photos app, where a thumbnail preview enlarges to fill the entire screen. In order to achieve this, the logic was to instantiate the full-screen image view, apply a scale + translation transform to make it the size and position of the thumbnail, then revert the transform to CGAffineTransformIdentity
while animating. Easy enough.
But we're not done yet. Just like in the Photos app, the aspect ratio of the thumbnail is independent from that of the image itself. What's more, in the Photos app the thumbnails are always square, whereas our thumbnails can have any arbitrary aspect ratio. In a plain UIImageView
(or any other UIView
subclass, for that matter), this is easily achieved like so:
[imageView setContentMode:UIViewContentModeScaleAspectFit];
However, we need an animated transition between the cropped image of the thumbnail and the full view. The solution here is to use the mask
property of CALayer
like so:
CALayer *mask = [CALayer layer];
// Set up the mask's bounds to correspond with the visible part of the thumbnail.
[imageView.layer setMask:mask];
We also wanted to enable users of Titanium to use thumbnails with rounded corners. That meant we had to use the cornerRadius
property on our full screen view. However, we couldn't just read the value from the thumbnail view, apply it to our full screen view and call it a day. Because our view was going to get scaled, we had to multiply the value by the inverse of the scale factor before applying it.
The major drawback of using CALayer properties is that, unlike CGAffineTransforms
, they cannot be animated using UIView
animation blocks. Instead, I had to create a CABasicAnimation
for each property I wanted to animated (mask
and cornerRadius
). Here's a quick example:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"];
animation.duration = 1.0;
animation.fromValue = @(200.0);
animation.toValue = @(320.0);
[mask addAnimation:animation forKey:@"bounds"];
The easiest, most straightforward way of providing scrolling and zooming capabilities to a view with arbitrarily-sized content is to use a UIScrollView
. In practice however, it proved too challenging to integrate into the custom animations, and an alternative solution was found.
The full screen view is composed of a black background view and an image view. Interaction is achieved using UIGestureRecognizers
that detect four different types of gestures:
- pan
- pinch
- tap
- double tap
The general structure of the code was derived from the Touches GestureRecognizers sample project (available here).
These two gesture recognizers work in tandem to provide direct manipulation of the image view. The UIPanGestureRecognizer
affects the center
property of the image view, while the UIPinchGestureRecognizer
applies a CGAffineTransformScale
to it.
In addition to the pinching and panning, two instances of UITapGestureRecognizer
handle single- and double-taps. A single tap will revert to the original zoom level and dismiss the view, and a double tap will zoom to the maximum zoom level.
In order for these to work alongside each other, the gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
delegate method is implemented to return YES
if the two gesture recognizers in question are the single-tap and the double-tap, respectively. One drawback of this solution is that it introduces a slight delay in the detection of a single-tap, while the system gives the user a chance to perform a double-tap.