Created
July 21, 2017 18:30
-
-
Save fvumbaca/3e532ba2253e694ff5a00dca0455254e to your computer and use it in GitHub Desktop.
Swift 3 UIView Animation Composer
This file contains hidden or 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
// | |
// 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