Skip to content

Instantly share code, notes, and snippets.

@fvumbaca
Created July 21, 2017 18:30
Show Gist options
  • Save fvumbaca/3e532ba2253e694ff5a00dca0455254e to your computer and use it in GitHub Desktop.
Save fvumbaca/3e532ba2253e694ff5a00dca0455254e to your computer and use it in GitHub Desktop.
Swift 3 UIView Animation Composer
//
// AnimationBuilder.swift
// CustomerAssist
//
// Created by Frank Vumbaca on 2017-07-20.
// Copyright © 2017 Redlab. All rights reserved.
//
import Foundation
import UIKit
/// Converts multiple, offset transition animations on UIViews into keyframe animations that work as expected
class ViewAnimationBuilder {
/// Delay for when the animation starts
let delay:Double
/// A function that will reset the view before applying transformations for consistancy
let resetFn: (UIView)->()
/// A list of all transitions to preform over the animation
private let transitions: [Transition]
// A collection of all keyframe start times (relative) that are identified as important. Should be kept in assending order
private let keyframeTimes: [Double]
/// Contains transition information and business logic for calculations
private struct Transition {
/// Start time of the transition
var startTime,
/// End time of the transition
endTime,
/// Initial value of the transition (before the element starts animating)
initialValue,
/// Final value of the transition (after the elemt has animated)
finalValue,
/// Offset of the transition
offset: Double
/// Function that applies the transition to the element
var transFn: (UIView, Double)->()
/// Tests if the transition has started by a timestamp
func hasStarted(at:Double) -> Bool {
return at > startTime
}
/// Tests if the transition has completed by a timestamp
func isDone(at:Double)->Bool {
return at >= endTime
}
/// Applies the transformation function to correctly translate the element for the keyframe at the provided time
func preformTransition(forTime:Double, on: UIView, reversed: Bool = false) {
if (!hasStarted(at: forTime)) {
// Transition has not started yet, so translate to starting value
transFn(on, offset + (!reversed ? initialValue : finalValue))
} else if (isDone(at: forTime)) {
// Transition has ended, so translate to the ending value
transFn(on, offset + (!reversed ? finalValue : initialValue))
} else {
// Is currently animating, so calculate and apply the value the element should be translated by
let duration = endTime - startTime // transition duration
let valueDiff = (finalValue - initialValue) * (reversed ? -1 : 1) // value diffrenece from start to end
transFn(on, offset + (forTime - startTime) / duration * valueDiff)
}
}
}
init(delay: Double, resetFn: @escaping (UIView)->()) {
self.delay = delay
self.resetFn = resetFn
self.transitions = []
self.keyframeTimes = [0]
}
/// Constructor for builder use
private init(delay: Double, resetFn:@escaping (UIView)->(), transitions: [Transition], keyframes: [Double]) {
self.delay = delay
self.resetFn = resetFn
self.transitions = transitions
self.keyframeTimes = keyframes
}
/**
Adds another animation instance to the builder, and returns a new builder with the transition added.
> **NOTE** Some transformations should be applied before others, as calculations are done in a transformation matrix.
> take care when selecting what animation to add when (ie: translate before rotating).
*/
func addAnimationTransition(startValue: Double, endValue: Double, startTime: Double, endTime: Double, valueOffset: Double = 0, applyTransformation:@escaping (UIView, Double)->()) -> ViewAnimationBuilder {
// Create the new transition
let newTransition = Transition(startTime: startTime, endTime: endTime, initialValue: startValue, finalValue: endValue, offset: valueOffset, transFn: applyTransformation)
// Clone the keyframes array
var newFrames = self.keyframeTimes + []
// Add important frames if not already in the list
if (!newFrames.contains(startTime)) {
newFrames += [startTime]
}
if (!newFrames.contains(endTime)) {
newFrames += [endTime]
}
// Return the new animation builder with the added transition
return ViewAnimationBuilder(delay: self.delay, resetFn: self.resetFn, transitions: self.transitions + [newTransition], keyframes: newFrames.sorted())
}
/**
> **NOTE: Only use this method when inside a UIView.animateKeyframes callback**
This method will calculate all necessary keyframes, slice transitions to work together over the entire animation, and queue them
*/
func registerAnimation(forView: UIView, reversed: Bool = false) {
for (frameStart, frameEnd) in zip(self.keyframeTimes, self.keyframeTimes.suffix(from: 1)) {
UIView.addKeyframe(withRelativeStartTime: frameStart , relativeDuration: frameEnd - frameStart, animations: {
self.resetFn(forView)
for transition in self.transitions {
transition.preformTransition(forTime: frameEnd, on: forView, reversed: reversed)
}
})
}
}
}
// MARK: - UIView Extensions
/// Adds utilities to UIView to make creating transitions less verbose
extension UIView {
// MARK: - static functional wrappers
/// Hard-sets a view's transformation matrix
static func resetView(withTransform: CGAffineTransform) ->((UIView)->()) {
func reset(forView: UIView) {
forView.transform = withTransform
}
return reset
}
/// Adds dx to the view's x value
static func translateX(ofView v: UIView, by dx: Double) {
v.translate(dx: dx, dy: 0)
}
/// Adds dy to the view's y value
static func translateY(ofView v: UIView, by dy: Double) {
v.translate(dx: 0, dy: dy)
}
/// Rotates a view by degrees
static func rotate(view v: UIView, by degrees: Double) {
v.rotate(degrees)
}
/// Scale a view
static func scale(view v: UIView, by scale: Double) {
v.scale(scale)
}
/// Sets the opacity of a view
static func setOpacity(ofView v: UIView, to scale: Double) {
v.alpha = CGFloat(scale)
}
// MARK: - Syntactic suggar for some transformations
/// Moves view to an absolute x and y
func moveTo(x: Double, y: Double) {
self.frame.origin = CGPoint(x: CGFloat(x), y: CGFloat(y))
}
/// Translate view by dx and dy
func translate(dx: Double, dy: Double) {
self.transform = self.transform.translatedBy(x: CGFloat(dx), y: CGFloat(dy))
}
/// Rotate the view by degrees
func rotate(_ degrees: Double) {
self.transform = self.transform.rotated(by: CGFloat(degrees / 360.0 * 2.0 * Double.pi))
}
/// Scale the view by a decimal
func scale(_ by: Double) {
self.transform = self.transform.scaledBy(x: CGFloat(by), y: CGFloat(by))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment