Make draggable UIViewController with swipe cards using custom transitions in iOS
How to implement interactive, distinguished interface considering possibilities of UIKit
There are two main things, that I will demonstrate and explain in this article:
UIKit supplies us with a short amount of built-in transitions, that we regularly use while implementing a transition from one view controller to another. The most popular are modal present and navigation push. Because of their simpleness and wide common usage, such transitions could be boring and won’t make users excited in case when app’s design is fully custom.
A great solution would be to use possibilities of UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning protocols that stand for creating custom interactive transitions.
The second thing is about frequently used UI component at iOS — UICollectionView and its cells. Usually, if talking about interaction with cells, it’s enough for us to handle clicks via
didSelectItemAt method of the delegate. But I want to show, how an injection of UIGestureRecognizer object can increase the potential of UICollectionView cells and distinguish it from the rest of the screen’s components.
Remark: I wrote this article to show the most important things and features, rather than teaching step-by-step implementation. So it assumes, that you feel confident with Swift language and know basics of UIKit.
Let’s start from transition
The project has a storyboard, that contains two view controllers. StartViewController(left) has a lot of subviews, just to show some content. But the view, that is interesting for us, has label “Open Cards View” and placed in the bottom. It will be responsible for catching gestures and presenting CardsViewController(right) by dragging or with a simple tap.
CardsViewController has collection view, responsible for showing cards and has few subviews to show different backgrounds based on transition’s progress.
A required thing to implement custom transition is to describe its mechanic’s rules at class, conformed to UIViewControllerAnimatedTransitioning.
For presenting transition create MiniToLargePresentingViewAnimator class, that inherits from NSObject due to UIViewControllerAnimatedTransitioning requirements. Also, you need to create MiniToLargeDismissingViewAnimator, responsible to provide custom dismiss transition. Both animators have pretty the same code, so let’s explore only presenting part.
MiniToLargePresentingViewAnimator has two properties:
initialY. With the first property, everything must be straightforward.
initialY needs more attention. When the user starts dragging bottom piece of card, the next view controller is being presented and his position starts from
initialY is the position of bottom card view.
This trick enables us to replace invisibly bottom card view with CardsViewController’s view and achieve the effect of a draggable card.
When CardsViewController is going to dismiss, it ended at
initialY as well.
Magic happens at
animateTransition(using transitionContext: UIViewControllerContextTransitioning) method, where described how the transition should work.
1. Getting view controllers, that are involved at transition. MiniToLargeAnimatable protocol is used here to avoid access to CardsViewController explicitly.
2. Setting initial frame and background appearance for presenting view controller.
3. This step is optional in general but required in our situation. Since you’re going to use custom
modalPresentationStyle and interactive presentation, view controller lifecycle does not work as expected in this case. You have to track these events yourself.
4. Standard animating block, used to change smoothly frame and background of subviews owned by presented view controller.
By implementing such two animators you’ll have custom transitions for presenting and dismissing of view controller. But wait, are they already interactive and you can drag bottom card? Not yet. One more class is required to make transition interactive with gestures.
Let’s make transition interactive
You need a new class called MiniToLargeViewInteractiveAnimator that inherits from UIPercentDrivenInteractiveTransition. The main thing, that this class does is adding a gesture recognizer to the desired view and updating of the custom transition(implemented at animators above) based on the percent of the swipe made by the user at target view.
Since this class is responsible for managing both present and dismiss, gesture recognizer must be added to appropriate controllers’ views.
1. Store view controllers, involved in transition for later usage. Add gesture recognizer to target view based on present/dismiss case. And slow down completion of the transition by setting
2. Removing gesture recognizer from view before deallocating animator since you don’t need it anymore. This step does not affect the transition.
3. Just handle all states of gesture and update custom transition based on swipe progress.
4. If you want to finish the transition earlier, you can check whether
percent reached the
threshold and call
finish() method. Let’s ignore call
prepareBeingDismissed() for now.
5. When the user ended dragging and transition was still in progress, you need to determine what to do based on
Let’s sum up what you have now. You created three classes, responsible for providing custom interactive transition: MiniToLargePresentingViewAnimator, MiniToLargeDismissingViewAnimator and MiniToLargeViewInteractiveAnimator. Now you need to figure out how to use all this stuff with our controllers.
In order to decouple CardsViewController from StartViewController due to common transition, let’s create a class, called MiniToLargeTransitionCoordinator. It conforms to UIViewControllerTransitioningDelegate and manages our draggable transition. StartViewController holds an instance of transitionCoordinator and calls only its two methods based on conditions.
prepareViewForCustomTransition(fromViewController: StartViewController) creates required bindings and interactive animators, instantiates CardsViewController and prepares it for draggable transition. Pay attention to
gestureView of animators. On present transition we want
bottomTriggerView of StartViewController to be responsive on pan gesture. And on dismiss case, hole
view of CardsViewController responds to gesture.
removeCustomTransitionBehaviour() is called when you want to remove the possibility to show CardsViewController, including dragging behavior. For instance, when there is no data to show.
And finally MiniToLargeTransitionCoordinator implements required methods of UIViewControllerTransitioningDelegate protocol. These methods will be triggered to replace default transition animation in case of
dismiss(…) calls from our view controllers.
The only unusual moment here is about
isTransitionInProgress check condition. It’s required because custom transition could be performed without interaction via a single tap on the bottom card at StartViewController or with manual dismiss at CardsViewController. In this case, methods must return nil for UIViewControllerInteractiveTransitioning.
Let’s dive into swipe UICollectionView cells
This is how CardCollectionViewCell looks at interface builder. In reality, there’s only one container, that holds different subviews. But I split it into two for providing better visualization. The left container is called
frontContentView and represents the content of certain item in array. Specifically, this view will be dragging. On the right side, there is
actionsView, that provides buttons to interact with the cell and its content.
In order to make CardCollectionViewCell or your own cell swipeable, inherit it from SwipingCollectionViewCell, which stands for providing swiping behavior.
CardCollectionViewCell is just a plain cell, that shows info to the user. It has one optional extra method
frontViewPositionChanged(on percent: CGFloat) from the parent cell, responsible to update properties of subviews while cell’s swiping is in the progress. If you don’t want any UI changes on swipe, this method could be omitted.
Also, there is required override property
swipeDistanceOnY. Or, in other words — it is the height of
actionsView. Using this property cell determines bounds of swipe area. There’re only two possible states of the cell during the dragging: at the initial position and at the top position.
Let’s look at SwipingCollectionViewCell implementation.
When the cell is loaded from xib file, an initial position of
frontContentView is saved as it will be needed later. Then pan gesture recognizer is added via
lazy property. When
frontContentView will be dragging up or down, visually it will be located outside of the cell’s bounds. So it’s required to set false for
clipsToBounds property, otherwise dragging view will be hidden by the bounds of the cell.
Next code will show to you what exactly is going on during the dragging of the cell:
1. When pan gesture is activated, it will have different states. The most interesting for us is
.changed state. Right here progress of
frontContentView movement is being calculated and based on it you update frames and other properties of cell’s subviews.
2. On the
.ended state you determine dragging direction and manually finish the animation.
3.1 and 3.2 methods move
frontContentView with animation to appropriate place based on conditions. The unusual thing, that you likely noticed inside both methods is CADisplayLink. It calls
animationDidUpdate() with refresh rate of the display. CADisplayLink enables to react fast to all changes that occur during the animation inside
4. You take
frontContentView frame at a specific time interval during the animation and based on this frame calculate progress of animation. Basically, it is similar to logic inside
.change state of
At this moment you have like a card draggable view controller with draggable, scrolling cards inside.
You need to allow different gestures to act simultaneously. Otherwise, it will be a gesture conflict leading to incorrect work of any of the components. Implementation of
shouldRecognizeSimultaneouslyWith method from UIGestureRecognizerDelegate protocol is a solution.
Now you know, how interactive transitions work under the hood and how easy it’s to make swiping cells. I believe, that with these bits of knowledge more new opportunities become available and it’s simpler to highlight your app from the crowd.
The full project is available on Github.
If you have any questions or suggestions, feel free to contact me by email or just leave a comment below.
Hope you found this article useful and inspiring. Thanks for reading!