Just another attempt to simulate App Store's Card transition:
Implementation details are available in slides under the MobileConf
folder, in the last section ('5 Phases of Interaction').
My previous repo is here. The new one is a total rewrite. It has better effect/performance, better code organization, and has fixes for some issues found in the previous repo.
All is done with native APIs (UIViewControllerAnimatedTransitioning
, etc.), no external libraries. This is NOT a library to install or ready to use, it's an experiementation/demo project to show how such App Store presentation might work.
- Status bar animation
- Very responsive card cell highlighting animation
- Card bouncing up animation (Two animations at work: spring for moving to place, linear for card expansion)
- Damping and duration depends on how far the card needs to travel on screen
- Drag down to dismiss when reach the top of content page
- Scroll back up to cancel the dismissal!
- Left screen edge pan to dismiss
Transition/PresentCardAnimator
: Animation code for presentation,Transition/DismissCardAnimator
: Animation code for dismissal,Transition/CardPresentationController
: Blur effect view and overall aspect of the presentation,ViewControllers/CardDetailViewController
: Interactive shrinking pan gesture code.ViewControllers/HomeViewController
: Home page, preparation code before presentation is at collectionView's didSelect delegate method.Misc/StatusBarAnimatableViewController
: Status bar animation (quick & dirty though, need to inherit from this parent vc.)
- Fix layout/top area on iPhone X
- Support continuous video/gif playing from home to detail page (This requires some work to use a whole view controller as a card cell content from the first page!)
- Add blurry close button at the top right of detail page
- Perfecting card bouncing up animation (still can't figure out how to achieve that smooth bounciness like the App Store.)
Here are some implementation details:
- The card cell needs to be very responsive to touch, so we must set
collectionView.delaysContentTouch = false
(it'strue
by default, to prevent premature cell highlighting, e.g., on table view). - Put scaling down animation in
touchesBegan
andtouchesCancellled/Ended
. .allowsUserInteraction
is needed in animation options, so that you can always continue to scroll immediately while the unhighlighted animation is taking place.
- Need to stop all animations, using
cardCell.layer.removeAllAnimations
. Also prevent any future highlighting animation with a flag. - Get current card frame (that is currently animated scaling down) with
cardCell.layer.presentation().frame
, then convert it to screen coordinates. - Get presented view controller (
CardDetailViewController
)'s view and position it with AutoLayout at the original card cell's position. - Hide original card cell's position.
- Simply animating frame/AutoLayout constraints with Spring animation won't work.
- Best alternative (that I can think of right now) is to animate with two different animation curves: linear for card expansion, and spring for moving up to place.
- Turns out you can animate different constraints in two animation blocks, like this:
// Animate constraints on the same view with different animation curves
UIView.animate(withDuration: 0.6 * 0.8) {
self.widthAnchor.constant = 200
self.heightAnchor.constant = 320
self.targetView.layoutIfNeeded()
}
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
self.topAnchor.constant = -200
self.targetView.layoutIfNeeded()
}) { (finished) in ... }
- Need to handle left screen edge pan and drag down pan.
- For drag down we'll add a new pan gesture. Make it able to detect simultaneously with
scrollView
's pan.- This means we need to carefully handle when the
dragDownMode
begins, save the starting drag point to calculate dragging progress, as it usually begins on.change
, not.began
.
- This means we need to carefully handle when the
- For left edge pan just use
UIScreenEdgePanGestureRecognizer
.
- For drag down we'll add a new pan gesture. Make it able to detect simultaneously with
- Give priority to left edge pan by:
dragDownPan.require(toFail: leftEdgePan)
scrollView.panGestureRecognizer.require(toFail: leftEdgePan)
- Note that the method
a.require(toFail: b)
is confusingly named. It actually meansa
must wait forb
to fail first before it can start. So just read it likea.wait(toFail: b)
when you see that. - To smoothly transition to shrinking mode when reach the top of scroll view, just use scrollView's delegate:
var draggingDownToDismiss = false // A flag to check mode
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if draggingDownToDismiss || (scrollView.isTracking && scrollView.contentOffset.y < 0) {
draggingDownToDismiss = true
scrollView.contentOffset = .zero // * This is important to make it stick at the top
}
scrollView.showsVerticalScrollIndicator = !draggingDownToDismiss
}
- Handle shrinking on drag using
UIViewPropertyAnimator
:
let shrinking = UIViewPropertyAnimator(duration: 0, curve: .linear, animations: {
self.view.transform = .init(scaleX: 0.8, y: 0.8)
self.view.layer.cornerRadius = 16
})
shrinking.pauseAnimation()
- Carefully handle
progress/fractionComplete
of the animator by understaning when corresponding gestures are began! Use a combination ofgesture.translation(in: _)
andgesture.location(in: nil)
, etc. - Reverse animation on drag down pan gesture ended/cancelled:
shrinking!.pauseAnimation()
shrinking!.isReversed = true
// Disable gesture until reverse closing animation finishes.
gesture.isEnabled = false
shrinking!.addCompletion { [unowned self] (pos) in
self.didCancelDismissalTransition()
gesture.isEnabled = true
}
shrinking!.startAnimation()
- Just do animation back to original cell's position.
If you're interested in a more visual guide to '5 Phases of Interaction', checkout MobileConf/slides
- This is hard to explain, but there's some space on card view top edge during presentation despite constant 0 of their topAnchors. What's weirder is that it's already unintentionally fixed by setting a top anchor's constant to value >= 1 (or <= -1). Setting it to any values in the range of (-1, 1) doesn't work.
- Blur effect view in the back seems to not showing up properly when we're in dismissal pan mode (especially on iOS 12). But sometimes it happens on iOS 11 too! Proobably due to my incomplete understanding of viewWillAppear/beginTransition/redraw life cycle.