Shift is a simple, declarative animation library for building complex view controller and view transitions in UIKit.
Shift can automatically transition matched views from one view controller to the next, by simply providing an id
to the source and destination views. Transitions like these can make transitions feel very fluid and natural and can help give context to the destination screen. Additional animations can be applied to the unmatched views that will be run during the transition.
Shift is very similar to Hero in that it can animate view controller transitions. It differs in that it is not tied to view controllers only. The animations can be applied to two different UIView
s regardless of whether an actual transition is occurring. Which can be useful if you are transitioning between two child view controllers, or just two plain UIView
subviews. It can also be plugged easily into custom transitions where you need to supply your own UIViewControllerAnimatedTransitioning
or UIPresentationController
. This can be very useful if the destination view controller does not cover the full screen.
All examples can be found in the /Examples
folder, and can be run via the /Examples/Examples.xcworkspace
.
To begin using Shift, you must enable it before presenting the view controller.
viewController.shift.enable()
present(viewController, animated: true, completion: nil)
This will cause Shift to take over for any transitions for the view controller. If you stop here, when presenting the view controller you will see that the view is presented with a fade animation. This is the defaultAnimation
, and is customizable. If none is desired it can be stopped by setting it to nil
. There are also multiple other defaults provided.
viewController.shift.defaultAnimation = DefaultAnimations.Scale(.down)
You can make your own custom default animations as well by conforming to DefaultShiftAnimation
, and setting the desired animations on the views. How to add custom additional animations will be covered later.
The duration of the transition animation is determinded by the baselineDuration
provided. This is a baseline for the duration and not the actual value that will be used. Each view will calculate a duration for its animations based off the animations being applied. In other words, the baselineDuration
is the minimum duration, and each view will add additional time on top if need be.
// Transition will now take about 1 second.
viewController.shift.baselineDuration = 1
The "toViews" and "fromViews", or the views that we are transitioning from and to, need to be added to the transition container. The order in which they are added can be tweaked, to give you control over which views are on top or bottom. This is done by the viewOrder
property. By default this will be auto
. Auto adds the views in the order you would expect. The view being transitioned to will be on top, and on dismissal the view that is being dismissed will be on top. However this may not be what you want. Using view order you can choose which views should be on top.
viewController.shift.viewOrder = .fromViewsOnTop
A matched view is where you have a view on the source view that needs to be animated to a view on the destination view. This can be done by supplying a matching id
to each view. During the transition, the source view's frame, and other common properties, will be animated to match the destinations.
firstView.shift.id = "blueView"
secondView.shift.id = "blueView"
That's it! The sourceView
will now be magically moved to match the destinationView
.
Note: None of the views are actually edited, snapshots of each are used.
When views are matched, their content may be different. How it is transitioned from the initial content to the new content can be customized. There are two different options for content animations, fade
and final
. This option is only valid on matched views, since unmatched views do not have content changes.
On fade the initial content will be faded out and the new content will be faded in.
view.shift.contentAnimation = .fade
On final the content will immediately show the final content.
view.shift.contentAnimation = .final
Matched views may change size during the transition. Since snapshots of the views are used when they are resized, the content may warp and stretch. You can customize the behavior of how the content is sized. There are two different sizing options, stretch
and final
.
With stretch, the content's size will directly match the view as it is changed to its new final frame.
view.shift.contentSizing = .stretch
With final, the content's size will be set to its final size instantly, and any size changes will not affect it.
view.shift.contentSizing = .final
If one view does not have a match, but needs to be animated during the transition, you can provide a number of additional animations. These animations will be applied during the transition, and will be automatically reversed on dismissal.
For example, if we want a view to fade and slide in from the left by 150 points, it can be done by applying the animations like so:
view.shift.animations
.fade()
.move(.right(150))
To view a full list of potential animations, please see
These animations can be setup to conditionally run based on some predicate. Each animation function has an optional parameter to supply a Condition
; it is always the last parameter. If the condition is not met, the animation will not be added.
To only have a view scale up when the view appears, but not scale back down while disappearing, do:
view.animations.scale(2, .onAppear)
To provide a custom condition, you can use the .filter
.
view.animations.scale(2, .filter{ ... })
When a view is animated during a transition, its snapshot must be added to a view within the transition. There are two different options for the superview
, parent
and container
. Each different superview can affect how the view appears to make it to its final position.
With parent, its superview
will be a view that most closly relates to the view's actual superview
in the original view heirarchy. If its superview
's position, is being animated, it will also be animated since it's a subview of the view being animated.
star.shift.superview = .parent
On container it will be added directly to the transition's container
view, and it will not be affected by the original superview
's position. parent
is the default choice, however matched views will always use container
regardless of the choice.
star.shift.superview = .container
If you need to supply your own UIViewControllerAnimatedTransitioning
or UIPresentationController
, it is very simply to incorporate Shift into the transition.
At the heart of every transition is the Animator
. It is responsible for performing the animations.
In your UIViewControllerAnimatedTransitioning
subclass, in the animateTransition(using transitionContext:)
function declare a new animator, and call animate.
let animator = Animator(
fromView: fromViewController,
toView: toViewController,
container: transitionContext.containerView,
isPresenting: true
)
animator.animate { complete in
toViewController.view.alpha = 1
transitionContext.completeTransition(complete)
}
See the default modal transition animator supplied for the full example.
In the examples project, the "Movie" view uses a custom UIPresentationController
to present a non-fullscreen context menu.
Shift is available through Swift Package Manager.
To add it via SPM, simply add https://github.com/wickwirew/Shift.git
to your list of packages in Xcode.
Shift is available through Carthage. To install it, simply add the following line to your Cartfile:
github "wickwirew/Shift"
Shift is available through CocoaPods. To install it, simply add the following lines to your Podfile:
use_frameworks!
pod 'ShiftTransitions'
- Hero was obviously a huge inspiration behind this library, so a ton of credit goes to lkzhao and the Contributors. This is merely another option if Hero does not nessecarily do everything you need.
- SamAtmoreMedia on Dribbble for the concept of the space example design.