UICollectionView custom transition: Pop In & Out

25 Dec

In October I attended to Pragma Conf 2015 and there was a talk about “library-oriented” programming that really struck me. The talk was about trying to generalise as much as possible the code and collect it into libraries to be able to easily reuse it. Of course I was already writing code in a way to make it easy to reuse… but I was not creating a library for every “chunk” of it. Since this “library-oriented” thing intrigued me I decided to give it a try: I started extracting any reusable pice of code inside my apps and creating a library out of it .

One of the first thing I extracted was a custom transition for UICollectionViews that I created for my app MyTVSerials which implement a nice pop effect when tapping a cell to trigger the transition (see the video below)… and this blog post is about it.

The library with the custom transition is already available on GitHub and it comes with a fancy demo app to see how it works and how to use it. The post will cover the basics about custom navigation controller transitions and will also explain how to implement the animation itself.

The key element for custom view controller transitions is the protocol UIViewControllerAnimatedTransitioning: a class conforming to this protocol is responsible for performing the transition (both adding the destination view to the view hierarchy and animating the process).

UIViewControllerAnimatedTransitioning defines 3 methods:

The first method, animateTransition:, is called when the transition has to be performed:

UIKit calls this method when presenting or dismissing a view controller. Use this method to configure the animations associated with your custom transition. You can use view-based animations or Core Animation to configure your animations.

The second one, animationEnded:, is optional and is called when the transition has finished:

UIKit calls this method at the end of a transition to let you know the results. Use this method to perform any final cleanup operations required by your transition animator when the transition finishes.

The last one, transitionDuration:, tells the framework the expected duration:

UIKit calls this method to obtain the timing information for your animations. The value you provide should be the same value that you use when configuring the animations in your animateTransition: method. UIKit uses the value to synchronize the actions of other objects that might be involved in the transition. For example, a navigation controller uses the value to synchronize changes to the navigation bar.

In my library PopInAndOutAnimator is the class conforming to UIViewControllerAnimatedTransitioning and is as well a subclass of NSObject since it part of the requirements to conform to the protocol.

PopInAndOutAnimator defines two private properties:

The first one, _operationType, is an enumeration defining the type of transition we are dealing with (values are Push, Pop and None… we are interested only in the first two) whilst the second, _transidionDuration, is a Double (NSTimeInterval is an alias for Double) representing the desired duration of the transition.

To initialize PopInAndOutAnimator the operation type is mandatory (to know whether we have to perform a push or a pop) whereas the duration is optional (if no duration is provided a default value of 0.4s is used). Here are the two available initializers:

To conform to UIViewControllerAnimatedTransitioning it implements the two mandatory methods transitionDuration and animateTransition:

As you can see transitionDuration simply return the value of the private property containing the duration of the transition whereas animateTransition use the value _operationType to choose which transition has to be performed (the Push or the Pop one).

Now… before digging into the methods that perform the animation (that are not part of the UIViewControllerAnimatedTransitioning protocol but have the purpose of making the code cleaner grouping the 2 different animations in different methods) we have to look to a key protocol I introduced in the library: CollectionPushAndPoppable

The UICollectionViewController involved in the custom transition must conform to it. The collectionView and the view properties are already implemented by any UICollectionViewController so there is nothing to do about them whilst the sourceCell must be implemented. The sourceCell property represent the the cell from which the animation should originate (push operation when cell is tapped) or should terminate (when going back). Of course the proper value of this property must be set before the transition begins. In the demo app i set the value of sourceCell in the method prepareForSegue assigning the sender to the sourceCell property… but is not mandatory.

Its now time to explore the methods of PopInAndOutAnimator that actually perform the animations: performPushTransition and performPopTransition. Both these methods receive in input an object conforming to UIViewControllerContextTransitioning which is the context object used to perform the transition.

The UIViewControllerContextTransitioning protocol’s methods provide contextual information for transition animations between view controllers. Do not adopt this protocol in your own classes, nor should you directly create objects that adopt this protocol. During a transition, the animator objects involved in that transition receive a fully configured context object from UIKit. Custom animator objects—objects that adopt the UIViewControllerAnimatorTransitioning or UIViewControllerInteractiveTransitioning protocol—should simply retrieve the information they need from the provided object.

A context object encapsulates information about the views and view controllers involved in the transition. It also contains details about the how to execute the transition.

Let’s begin with performPushTransition… as mentioned a context object is received in input and the very first thing done is a guard statement to retrieve the destination view and the container view which are the only essential elements to perform the transition. The container view is where the destination view and all the views participating in the animation must be placed (at the and of the transition the only view that should be left inside is the destination one).

The container view acts as the superview of all other views (including those of the presenting and presented view controllers) during the animation sequence. UIKit sets this view for you and automatically adds the view of the presenting view controller to it. The animator object is responsible for adding the view of the presented view controller, and the animator object or presentation controller must use this view as the container for all other views involved in the transition.

If for any reason the container view or the destination view are missing the transition simply fails (this should never happen though).

Second step is another guard that retrieve all the elements necessary for the animation: if anyone is missing the transition will be performed but without the animation.

Now that everything needed is available first thing to do is to add the destination view to the container view:

It is important to do so before taking any screenshot to let the constraints to be applied to the content of the view (if the screenshot was taken before adding this view to the container it would appear without the constraints applied).

With the destination view added to the container view the transition itself is completed, what comes next is just about preparing and performing the animation to make the transition fancy.

Since my animation involves view resizing the easiest way to deal with it is to use screenshots of the source and destination views, animate the screenshots and then just remove them once the animation is completed.

To take the screenshots of the views I created an extension of UIView adding a computed property called screenshot of type UIImage:

Next step is to create a screenshot of the destination view, set its size equals to the cell of the collection view and position it right on top of it:

First thing to do is to create an UIImageView with the screenshot of the destination view as image, after that we set the image view’s frame equals to the cell’s one (remember that a frame consists in both size an position). It is very important to understand the last 2 steps of this piece of code: since the cells of a collection view are contained inside a scroll view their frame’s position is relative to the scroll view hence to be used inside the container view (which is the view that will handle the animation) we must do a conversion…  luckily the UIView’s method convertPoint is exactly what we are looking for:

It gets a point of the view on which the method is called and convert it into a point of the view passed as second argument:

The view into whose coordinate system point is to be converted. If view is nil, this method instead converts to window base coordinates. Otherwise, both view and the receiver must belong to the same UIWindow object.

Once we got the coordinates inside the container we update the position of the screenshot view to use them… this way no matter ho much we scrolled the collection view the screenshot view will always perfectly overlap the tapped cell.

Next step is to create a second screenshot view containing a picture of the source view (the collection view cell):

Since the two screenshot views must overlap we can simply assign the same frame of the previous screenshot to this second one.

Now let’s add the the two screenshot views to the container view and set the initial state for all the views involved in the animation:

The destination view is hidden because we want to show it only after the completion of the animation, the screenshot of the destination view, on the other hand, shouldn’t need to be hidden because it lays underneath the screenshot of the cell and will appear as the screenshot of the cell fade out… but, for reasons that I ignore, in some cases if it’s not hidden at the beginning of the animation it causes a brief flickering. The solution was to set it hidden at the beginning of the animation and change again the hidden status right after the animation started (I don’t like it too much but it works).

What’s now left is to set up and start the animation… to animate something the UIView static method animateWithDuration is what we are looking for:

UIView has many variations of animateWithDuration: common elements among all are  the duration for the animation and a block telling the final status of the transformation that has to be animated. In the case of my animation I picked the one with the “spring” parameters to add a “bouncing” effect to the transformation:

The parameter used are:

  • duration: the one set at init time
  • delay: none
  • usingSpringWithDamping: this parameter define how “intense” is the oscillation added to the animation (the range of values goes from 1 to 0 where 1 is no oscillation at all and 0 is the maximum oscillating rate), I chose a value of 0.7 to add just a slight bouncing effect
  • initialSpringVelocity: the initial spring velocity. For smooth start to the animation, match this value to the view’s velocity as it was prior to attachment (A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5). Since my view has no velocity I used 0 as value.
  • options: A mask of options indicating how you want to perform the animations. I don’t need any specific option so it is empty
  • animations: A block object containing the changes to commit to the views. This is where you programmatically change any animatable properties of the views in your view hierarchy. This block takes no parameters and has no return value.
  • completion: A block object to be executed when the animation sequence ends. Here I put the clean-up code

To accomplish the animation the changes to perform in the animation block are to set the alpha of the cell’s screenshot equals to 0 to let it fade out letting the screenshot of the destination view to appear and set the frame of of both the screenshot views equals to the screen’s one with an origin point of (0,0) to let the destination view becoming “full screen“. In the completion block all the screenshot views are removed from the container view (they are now useless) and the actual destination view is displayed. The animation is now completed (to check the entire performPushTransition method check the project available on GitHub)!

The performPopTransition method, which is performing the inverted animation of the Pop one, is quite similar… it begins with two guard statements to retrieve all the required elements from the context:

Then adds the destination view to the container, creates a screenshot of both the source view and the destination one (the cell of the collection view) and put them into the container view:

Then the initial status of the animation is defined:

The screenshot of the destination view (the cell of the collection view) has its alpha set to 0 (to be transparent) whereas both the source view and the actual cell are hidden.

Last step before triggering the animation is to convert again the coordinate of the cell inside the collection view into the ones of the container:

Now everything is ready to trigger the animation… we are going to use the same UIView static method animateWithDuration as before with the same exact parameters but for the animation block:

This time we set the alpha of the screenshot of the destination view (the cell) to 1 to let it fade in and the frames of all the screenshots equals to the cell (to have the zoom out effect converging toward the cell position) but with the origin point converted into the container’s coordinates. Now the Pop animation is completed as well.

All the protocols and classes necessary to the custom transition are now ready… To use it in an applications all that is left to do, since the implementation of the protocol CollectionPushAndPoppable has been tackled before, is to define a navigation controller delegate that will return the animator when a transition is triggered. Any class can be the delegate of the navigation controller if it conforms to the protocol UINavigationControllerDelegate… I usually pick the collection view controller itself implementing the method:

Last thing is to assign the navigation controller its delegate… I suggest to do it in the viewDidLoad methods of the view controller (in the demo app I did so):

And we are done… everything has been explained: how to use the custom transition and how it is working “under the hood“. Inside the project available on GitHub check the demo application to better understand how to use the custom transition in case this post was not enough.

The demo app is a simple collection view with pictures taken by the Hubble Space Telescope that once tapped are going full screen:

  • Coração Tricolor

    Why doesn’t pictureView.image width conform to auto-layout settings? Only it’s hight does..

  • Jessica May

    Is there a reason you create your own screenshot function instead of using YourView.snapshotView(afterScreenUpdates: false)?
    https://developer.apple.com/documentation/uikit/uiview/1622531-snapshotview

    • Because that method was not working in my particular scenario (it was returning a black screenshot), or at least it was not working when the library was written!