iOS Advanced Animation Techniques: Custom Transitions and Interactive Animations
Published:
Creating engaging iOS applications requires sophisticated animation techniques that go beyond basic transitions. This guide explores advanced animation patterns, custom transitions, interactive animations, and performance optimization techniques that elevate user experience to the next level.
1. Custom View Controller Transitions
import UIKit
// MARK: - Custom Transition Protocol
protocol CustomTransitionDelegate: AnyObject {
func transitionDidComplete()
}
// MARK: - Custom Transition Animator
class CustomTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let isPresenting: Bool
weak var delegate: CustomTransitionDelegate?
init(isPresenting: Bool, delegate: CustomTransitionDelegate? = nil) {
self.isPresenting = isPresenting
self.delegate = delegate
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from),
let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
let containerView = transitionContext.containerView
if isPresenting {
// Presenting animation
containerView.addSubview(toView)
// Initial state
toView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
toView.alpha = 0
// Animate to final state
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
options: .curveEaseInOut) {
toView.transform = .identity
toView.alpha = 1
} completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.delegate?.transitionDidComplete()
}
} else {
// Dismissing animation
containerView.insertSubview(toView, belowSubview: fromView)
// Animate from current state to dismissed state
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
options: .curveEaseInOut) {
fromView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
fromView.alpha = 0
} completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.delegate?.transitionDidComplete()
}
}
}
}
// MARK: - Interactive Transition Controller
class InteractiveTransitionController: UIPercentDrivenInteractiveTransition {
private var shouldCompleteTransition = false
private weak var viewController: UIViewController?
func attach(to viewController: UIViewController) {
self.viewController = viewController
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
viewController.view.addGestureRecognizer(panGesture)
}
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else { return }
let translation = gesture.translation(in: view)
let progress = translation.y / view.bounds.height
switch gesture.state {
case .began:
viewController?.dismiss(animated: true)
case .changed:
update(progress)
shouldCompleteTransition = progress > 0.5
case .ended, .cancelled:
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
}
// MARK: - Custom Transition Manager
class CustomTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
private let interactiveController = InteractiveTransitionController()
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransitionAnimator(isPresenting: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransitionAnimator(isPresenting: false)
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactiveController
}
func attachInteractiveController(to viewController: UIViewController) {
interactiveController.attach(to: viewController)
}
}
// MARK: - Example View Controllers
class SourceViewController: UIViewController {
private let transitionManager = CustomTransitionManager()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
setupUI()
}
private func setupUI() {
let presentButton = UIButton(type: .system)
presentButton.setTitle("Present Custom Transition", for: .normal)
presentButton.addTarget(self, action: #selector(presentViewController), for: .touchUpInside)
presentButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(presentButton)
NSLayoutConstraint.activate([
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc private func presentViewController() {
let destinationVC = DestinationViewController()
destinationVC.transitioningDelegate = transitionManager
destinationVC.modalPresentationStyle = .fullScreen
transitionManager.attachInteractiveController(to: destinationVC)
present(destinationVC, animated: true)
}
}
class DestinationViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGreen
setupUI()
}
private func setupUI() {
let dismissButton = UIButton(type: .system)
dismissButton.setTitle("Dismiss", for: .normal)
dismissButton.addTarget(self, action: #selector(dismissViewController), for: .touchUpInside)
dismissButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dismissButton)
NSLayoutConstraint.activate([
dismissButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
dismissButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc private func dismissViewController() {
dismiss(animated: true)
}
}
2. Advanced SwiftUI Animations
import SwiftUI
// MARK: - Custom Animation Modifier
struct ShakeAnimation: ViewModifier {
@State private var isShaking = false
func body(content: Content) -> some View {
content
.offset(x: isShaking ? -5 : 0)
.animation(
Animation.easeInOut(duration: 0.1)
.repeatCount(5, autoreverses: true),
value: isShaking
)
.onTapGesture {
isShaking.toggle()
}
}
}
// MARK: - Morphing Shape Animation
struct MorphingShape: View {
@State private var morphing = false
var body: some View {
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.scaleEffect(morphing ? 1.5 : 1.0)
.animation(
Animation.easeInOut(duration: 2.0)
.repeatForever(autoreverses: true),
value: morphing
)
RoundedRectangle(cornerRadius: morphing ? 50 : 0)
.fill(Color.red)
.frame(width: 100, height: 100)
.scaleEffect(morphing ? 0.5 : 1.0)
.animation(
Animation.easeInOut(duration: 2.0)
.repeatForever(autoreverses: true)
.delay(1.0),
value: morphing
)
}
.onAppear {
morphing = true
}
}
}
// MARK: - Interactive Particle System
struct ParticleSystem: View {
@State private var particles: [Particle] = []
@State private var timer: Timer?
var body: some View {
ZStack {
ForEach(particles) { particle in
Circle()
.fill(particle.color)
.frame(width: particle.size, height: particle.size)
.position(particle.position)
.opacity(particle.opacity)
}
}
.onAppear {
startParticleSystem()
}
.onDisappear {
timer?.invalidate()
}
}
private func startParticleSystem() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
addParticle()
updateParticles()
}
}
private func addParticle() {
let particle = Particle()
particles.append(particle)
// Remove old particles
if particles.count > 50 {
particles.removeFirst()
}
}
private func updateParticles() {
for i in particles.indices {
particles[i].update()
}
}
}
struct Particle: Identifiable {
let id = UUID()
var position: CGPoint
var velocity: CGPoint
var color: Color
var size: CGFloat
var opacity: Double
var life: Double
init() {
position = CGPoint(x: 200, y: 400)
velocity = CGPoint(x: Double.random(in: -5...5), y: Double.random(in: -10...(-5)))
color = [.red, .blue, .green, .yellow, .purple].randomElement()!
size = CGFloat.random(in: 5...15)
opacity = 1.0
life = 1.0
}
mutating func update() {
position.x += velocity.x
position.y += velocity.y
velocity.y += 0.5 // Gravity
life -= 0.02
opacity = life
}
}
// MARK: - Custom Transition in SwiftUI
struct CustomTransition: ViewModifier {
let isActive: Bool
func body(content: Content) -> some View {
content
.scaleEffect(isActive ? 1.0 : 0.5)
.opacity(isActive ? 1.0 : 0.0)
.rotationEffect(.degrees(isActive ? 0 : 180))
.animation(
Animation.spring(response: 0.6, dampingFraction: 0.8, blendDuration: 0),
value: isActive
)
}
}
// MARK: - Advanced Animation View
struct AdvancedAnimationView: View {
@State private var isAnimating = false
@State private var dragOffset = CGSize.zero
@State private var rotation: Double = 0
var body: some View {
VStack(spacing: 30) {
// Morphing Shape
MorphingShape()
.frame(height: 200)
// Interactive Card
RoundedRectangle(cornerRadius: 20)
.fill(LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 200, height: 150)
.scaleEffect(isAnimating ? 1.1 : 1.0)
.rotation3DEffect(
.degrees(rotation),
axis: (x: 0, y: 1, z: 0)
)
.offset(dragOffset)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation
rotation = Double(value.translation.x / 10)
}
.onEnded { _ in
withAnimation(.spring()) {
dragOffset = .zero
rotation = 0
}
}
)
.onTapGesture {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
isAnimating.toggle()
}
}
// Particle System
ParticleSystem()
.frame(height: 200)
// Animated Button
Button("Animate") {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
isAnimating.toggle()
}
}
.buttonStyle(AnimatedButtonStyle())
}
.padding()
}
}
// MARK: - Custom Button Style
struct AnimatedButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
3. Performance-Optimized Animations
import UIKit
import QuartzCore
// MARK: - Animation Performance Monitor
class AnimationPerformanceMonitor {
static let shared = AnimationPerformanceMonitor()
private var displayLink: CADisplayLink?
private var frameCount = 0
private var lastFrameTime: CFTimeInterval = 0
func startMonitoring() {
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkFired))
displayLink?.add(to: .main, forMode: .common)
}
func stopMonitoring() {
displayLink?.invalidate()
displayLink = nil
}
@objc private func displayLinkFired() {
frameCount += 1
let currentTime = CACurrentMediaTime()
if currentTime - lastFrameTime >= 1.0 {
let fps = Double(frameCount) / (currentTime - lastFrameTime)
print("Animation FPS: \(fps)")
if fps < 55 {
print("⚠️ Low animation performance detected")
}
frameCount = 0
lastFrameTime = currentTime
}
}
}
// MARK: - Optimized Layer Animations
class OptimizedAnimationView: UIView {
private let animatedLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayer()
}
private func setupLayer() {
animatedLayer.backgroundColor = UIColor.blue.cgColor
animatedLayer.cornerRadius = 25
animatedLayer.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
// Optimize for animations
animatedLayer.shouldRasterize = true
animatedLayer.rasterizationScale = UIScreen.main.scale
layer.addSublayer(animatedLayer)
}
func performOptimizedAnimation() {
// Use CABasicAnimation for better performance
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 2.0
animation.duration = 1.0
animation.autoreverses = true
animation.repeatCount = Float.infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animatedLayer.add(animation, forKey: "scaleAnimation")
}
func performComplexAnimation() {
// Group multiple animations for better performance
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.0
scaleAnimation.toValue = 1.5
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.fromValue = 0
rotationAnimation.toValue = Double.pi * 2
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.5
let group = CAAnimationGroup()
group.animations = [scaleAnimation, rotationAnimation, opacityAnimation]
group.duration = 2.0
group.autoreverses = true
group.repeatCount = Float.infinity
animatedLayer.add(group, forKey: "complexAnimation")
}
}
// MARK: - Custom Easing Functions
class CustomEasing {
static func elasticEasing(t: Double) -> Double {
let c4 = (2 * Double.pi) / 3
return t == 0 ? 0 : t == 1 ? 1 : pow(2, -10 * t) * sin((t * 10 - 0.75) * c4) + 1
}
static func bounceEasing(t: Double) -> Double {
if t < 1 / 2.75 {
return 7.5625 * t * t
} else if t < 2 / 2.75 {
let t2 = t - 1.5 / 2.75
return 7.5625 * t2 * t2 + 0.75
} else if t < 2.5 / 2.75 {
let t2 = t - 2.25 / 2.75
return 7.5625 * t2 * t2 + 0.9375
} else {
let t2 = t - 2.625 / 2.75
return 7.5625 * t2 * t2 + 0.984375
}
}
}
// MARK: - Custom Animation with Easing
extension UIView {
func animateWithCustomEasing(duration: TimeInterval, easing: @escaping (Double) -> Double, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
let animation = CABasicAnimation(keyPath: "transform")
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0.1, 0.25, 1.0)
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setCompletionBlock {
completion?(true)
}
animations()
CATransaction.commit()
}
}
4. Interactive Gesture-Based Animations
import UIKit
// MARK: - Interactive Animation Controller
class InteractiveAnimationController: UIViewController {
private let animatedView = UIView()
private var animator: UIViewPropertyAnimator?
private var progress: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
setupAnimatedView()
setupGestures()
}
private func setupAnimatedView() {
animatedView.backgroundColor = .systemBlue
animatedView.layer.cornerRadius = 25
animatedView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animatedView)
NSLayoutConstraint.activate([
animatedView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
animatedView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
animatedView.widthAnchor.constraint(equalToConstant: 100),
animatedView.heightAnchor.constraint(equalToConstant: 100)
])
}
private func setupGestures() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
animatedView.addGestureRecognizer(panGesture)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
animatedView.addGestureRecognizer(tapGesture)
}
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
let progress = translation.y / view.bounds.height
switch gesture.state {
case .began:
startAnimation()
case .changed:
updateAnimation(progress: progress)
case .ended, .cancelled:
finishAnimation(shouldComplete: progress > 0.5)
default:
break
}
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
performSpringAnimation()
}
private func startAnimation() {
animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut) {
self.animatedView.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
self.animatedView.backgroundColor = .systemRed
}
}
private func updateAnimation(progress: CGFloat) {
animator?.fractionComplete = progress
}
private func finishAnimation(shouldComplete: Bool) {
if shouldComplete {
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
} else {
animator?.isReversed = true
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}
}
private func performSpringAnimation() {
UIView.animate(withDuration: 0.6,
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.5,
options: .allowUserInteraction) {
self.animatedView.transform = CGAffineTransform(rotationAngle: .pi)
self.animatedView.backgroundColor = [.systemBlue, .systemGreen, .systemPurple, .systemOrange].randomElement()!
}
}
}
Summary
Advanced iOS animation techniques include:
- Custom Transitions: Implement custom view controller transitions with interactive gestures
- SwiftUI Animations: Create complex animations using SwiftUI’s declarative syntax
- Performance Optimization: Monitor and optimize animation performance
- Interactive Animations: Build gesture-driven animations that respond to user input
- Custom Easing: Implement custom easing functions for unique animation curves
- Particle Systems: Create dynamic particle effects for engaging user experiences
By mastering these techniques, you can create iOS applications with smooth, engaging animations that enhance user experience and set your app apart from the competition.
Share on
Twitter Facebook LinkedIn☕ Buy me a coffee! 💝
If you found this article helpful, consider buying me a coffee to support my work! 🚀