Animations can be the final touch that make your app feel polished and complete. But getting animations right can sometimes be tricky. You want to avoid animations for the sake of having an animation, and you also want to avoid jarring animations that just leave the user feeling disoriented and confused. Animations should be subtle and give the user a hint of what just happened, or what possible actions they can and cannot take. A great example of this is the animations in the new iOS 11 App Store app – particularly on the Today tab. Pushing down on a cell gently makes the size smaller as if you’re pushing it down, and releasing your finger provides a beautiful transition into the story view controller. The user is fully aware of the context of what just happened, as it looks great too.
You’ll notice also that the animations don’t at all feel robotic and instead feel very natural and glide well. So today, let’s take a look at creating various UIView animations to get a better understanding of how they can be done.
First let’s take a look at that push in animation you get when you tap a cell on the Today view. This can be accomplished very easily by just creating a simple UIView.animate(withDuration:…) block and changing the transform property on the view’s layer.
To revert, we can create a new method that just resets the layer’s transform
Now what you can also do is adjust that first tapInAnimate() function to call tapOutAnimated() in its completion if shouldRevert is true.
Now let’s move onto something more subtle that’s actually been around a while – parallax. The effect here is the same one that you get on icons on your home screen when you move your phone. This method creates a UIMotionEffectGroup and adds it to the view.
Now just call yourView.addParalaxEffect() and when you start moving your phone around, you’ll get a subtle parallax effect added.
There are a few more, including a wobble animation and ‘tweak’ (I couldn’t think of a better word) animation that feels like when you hit a question block in a 2D Mario game. You can check them all out in the file I’ve linked to at the bottom of this post.
You can also apply these effects to a cell in a UICollectionView, and the property UICollectionView$bringSubview(toFront: UIView) might be useful in making the animations not clip underneath any other cells in the collection view.
Happy Christmas! ❄️🎅☃️
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Created by Jordan.Dixon on 30/11/2017. | |
// Copyright © 2017 Jordan.Dixon. All rights reserved. | |
// | |
import UIKit | |
// MARK: Paralax effect | |
internal extension UIView { | |
func addParalaxEffect(amount: Int = 20) { | |
let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis) | |
horizontal.minimumRelativeValue = -amount | |
horizontal.maximumRelativeValue = amount | |
let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis) | |
vertical.minimumRelativeValue = -amount | |
vertical.maximumRelativeValue = amount | |
let group = UIMotionEffectGroup() | |
group.motionEffects = [horizontal, vertical] | |
addMotionEffect(group) | |
} | |
} | |
// MARK: Tweak | |
internal extension UIView { | |
internal enum TweakDirection { | |
case up, down, left, right | |
} | |
func tweak(offset: CGFloat = 30, inDirection direction: TweakDirection = .down) { | |
let cellAnimation = createTweakAnimation(for: self, to: offset, direction: direction) | |
self.layer.add(cellAnimation, forKey: "position") | |
} | |
private func createTweakAnimation(for view: UIView, to offset: CGFloat, direction: TweakDirection) -> CABasicAnimation { | |
let animation = CABasicAnimation(keyPath: "position") | |
animation.duration = 0.1 | |
animation.repeatCount = 0 | |
animation.autoreverses = true | |
animation.fromValue = CGPoint(x: view.center.x, y: view.center.y) | |
switch direction { | |
case .up: | |
animation.toValue = CGPoint(x: view.center.x, y: view.center.y – offset) | |
case .down: | |
animation.toValue = CGPoint(x: view.center.x, y: view.center.y + offset) | |
case .left: | |
animation.toValue = CGPoint(x: view.center.x – offset, y: view.center.y) | |
case .right: | |
animation.toValue = CGPoint(x: view.center.x + offset, y: view.center.y) | |
} | |
return animation | |
} | |
} | |
// MARK: Wobble | |
internal extension UIView { | |
func wobble(duration: CFTimeInterval = 0.07, repeatCount: Float = 3) { | |
let animation = CAKeyframeAnimation(keyPath: "transform") | |
let wobbleAngle: CGFloat = 0.09 | |
let valLeft = CATransform3DMakeRotation(wobbleAngle, 0, 0, 1) | |
let valRight = CATransform3DMakeRotation(-wobbleAngle, 0, 0, 1) | |
animation.values = [valLeft, valRight] | |
animation.autoreverses = true | |
animation.duration = duration | |
animation.repeatCount = repeatCount | |
layer.add(animation, forKey: "transform") | |
} | |
} | |
// MARK: Tap In/Out | |
internal extension UIView { | |
func tapInAnimated(shouldRevert: Bool = true, duration: TimeInterval = 0.2) { | |
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseOut, .allowUserInteraction], animations: { | |
self.layer.transform = CATransform3DMakeScale(0.8, 0.8, 0.8) | |
}, completion: { didAnimate in | |
if shouldRevert { self.tapOutAnimated() } | |
}) | |
} | |
func tapOutAnimated(duration: TimeInterval = 0.5, withDelay delay: Double = 0) { | |
let damping: CGFloat = delay == 0 ? 1.0 : 0.5 | |
UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: damping, options: [.curveEaseOut, .allowUserInteraction], animations: { | |
self.layer.transform = CATransform3DIdentity | |
}, completion: nil) | |
} | |
} |