Now you've seen how easy it is to build a custom view controller transition animation, let's dive in further and add some custom interaction.
Transition offers you two approaches:
- DIY: implement an object that conforms to the
TransitionInteractionController
protocol - Drop-in: use Transition's built-in
PanInteractionController
for basic pan-gesture interaction
Want to take it easy? just skip to 2.
Either way, with custom interaction comes the requirement for an InteractiveTransitionOperationDelegate
.
The requirements are quite straightforward:
- What gesture should drive the interaction?
- What kind of operation (navigation push / pop, modal present / dismiss, tabbar index change) should initiate when the gesture is recognized?
- What is the progress of the transition based on the change of the recognized gesture?
- When the gesture ends, should the transition complete or cancel?
We start by creating an object that conforms to TransitionInteractionController
.
You'll be required to implement several functions, but everything starts with the gestureRecognizer
. We will use a UIPanGestureRecognizer
, which you can easily expose as follows:
private let panGestureRecognizer = UIPanGestureRecognizer()
public var gestureRecognizer: UIGestureRecognizer { return panGestureRecognizer }
The first line instantiates our panGestureRecognizer
for internal use in our object, the second one is in accordance with the TransitionInteractionController
protocol.
The TransitionController
will register as target of your gestureRecognizer. When the gesture is recognized, it'll ask your TransitionInteractionController
which TransitionOperation
should be initiated based on the gesture (this can be no action at all too).
You answer by implementing:
func operationForInteractiveTransition() -> TransitionOperation
Our pan gesture has a translation (in: view...) that can be used to determine in which direction the gesture was made. You convert this to whatever TransitionOperation
you prefer, for example .navigation(.push)
, or .none
if the gesture was in the wrong direction.
The gesture recognizer will frequently call its targets during interaction, causing your TransitionInteractionController
to be asked to translate the current position or movement of the gesture into the progress of the transition. You do this in:
func progress(in transitionOperationContext: TransitionOperationContext) -> TransitionProgress
Your answer can either represent a small step relative to the last update that will be added to the current fractionComplete
of the transition, or an overall fractionComplete
that will be used as the new corresponding value for the transition.
When the gesture ends, you want the transition to nicely animate towards the end, completing the transition.
However, it might be that the gesture movement was insufficient to decently complete the transition operation, or the movement went backwards halfway the transition. In such cases you might wish to reverse the transition, effectively rolling it back to the start, cancelling the transition operation.
Answer accordingly by implementing:
func completionPosition(in transitionOperationContext: TransitionOperationContext,
fractionComplete: AnimationFraction) -> UIViewAnimatingPosition
For your pleasure we have added an easy to use TransitionInteractionController
for basic pan gestures, called the PanInteractionController
. It can be used for any kind of transition (navigation, modal, tabBar).
For navigation and modal transitions, you initialize it with
PanInteractionController(forNavigationTransitionsAtEdge: TransitionScreenEdge)
or
PanInteractionController(forModalTransitionsAtEdge: TransitionScreenEdge)
respectively. The TransitionScreenEdge
(.top
, .right
, .bottom
or .left
) designates the edge of the screen from which new viewControllers will be presented or towards which they will be dismissed. To mimic native navigationController transitions, you would set this to .right
(at least for left-to-right languages), and for modal transitions this would be .bottom
. However you can now set this to whatever you like!
For tabBar transitions a different initializer is available that corresponds to the direction of items on the tabBar. To initialize one, call:
PanInteractionController.forTabBarTransitions(rightToLeft: Bool = false)
By default this is configured for left-to-right language apps where the tabBar item indices increase from left to right.
The PanInteractionController
has a built-in completionThreshold
(default: 1/3 of the transition progress) above which the transition will complete, but below which the transition will rewind to the start and cancel the transition operation. You can set this to anything between 0 and 1.
Another default feature is the "flick", a swift movement at the end of the gesture that has enough velocity to complete the transition in the direction of the flick (this might be towards the end but also towards the start). You can adjust the minimum velocity (expressed in points per second) for recognizing an ended pan gesture as a flick. You can also turn off this feature completely.
Depending on how you implement the interaction
part of your Transition
(which describes how the "Shared Element" should animate and move – more info here) you might want the TransitionInteractionController
to update the progress either as small step relative to the last update, or as an overall fraction complete. By default, the PanInteractionController
updates the progress as fraction complete, but you can switch this to progress step by setting updateProgressAsStep
to true.
The operation resulting from your recognized gesture can be any of the following:
- Navigation push
- Navigation pop
- Modal present
- Modal dismiss
- Increase TabBar index
- Decrease TabBar index
The TransitionController
leaves it up to you to appropriately respond to whatever applies for your configuration (you can only use a TransitionController
and associates for a single type – navigation, modal or tabBar – at a time).
The main reason for this is that Transition doesn't know which ViewController to instantiate when pushing or presenting. The delegate comes in three flavors:
func performOperation(operation: UINavigationControllerOperation,
forInteractiveTransitionIn controller: UINavigationController,
gestureRecognizer: UIGestureRecognizer)
}
func performOperation(operation: UITabBarControllerOperation,
forInteractiveTransitionIn controller: UITabBarController,
gestureRecognizer: UIGestureRecognizer)
}
func viewControllerForInteractiveModalPresentation(by sourceViewController: UIViewController,
gestureRecognizer: UIGestureRecognizer) -> UIViewController
}
🤔 Hey, that last one looks different!
Yes. For navigation and tabBar, leaving the operation up to you suffices, but for modal transitions, the correct transitioningDelegate
and modalPresentationStyle
must be set on the correct ViewController, at the correct moment. Therefore the operationDelegate is simply asked for the ViewController that needs to be presented, and the TransitionController
ensures it is presented correctly, with your custom transition.
You'll have to pass your new-found interactionController and operationDelegate to the TransitionController
. For our simple custom animation we used the initializer:
TransitionController(forTransitionsIn: navigationController,
transitionsSource: transitionsSource)
To make it interactive, change to this initializer:
TransitionController(
forInteractiveTransitionsIn navigationController: UINavigationController,
transitionsSource: TransitionsSource,
operationDelegate: InteractiveNavigationTransitionOperationDelegate,
interactionController: TransitionInteractionController)
For interactive tabBar transitions a similar initializer is available. For interactive modal transitions, the sourceViewController
(the one on which you'd call present(_, animated:, completion:)
) is required to also be the InteractiveModalTransitionOperationDelegate
, so the initializer signature is slightly different.
Feeling lucky? Let's dive in further and add a shared element!