-
-
Save tkersey/97ce46b8d7d6d5660046185a750649a8 to your computer and use it in GitHub Desktop.
Animations As Semiring
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
import Foundation | |
import UIKit | |
import PlaygroundSupport | |
// -------------------------------------------------------------------------------- | |
// MARK: - operators | |
// -------------------------------------------------------------------------------- | |
precedencegroup MonoidComposePrecedence { | |
associativity: left higherThan: AssignmentPrecedence lowerThan: AdditionPrecedence | |
} | |
precedencegroup SemiringMultiplicationPrecedence { | |
associativity: left higherThan: NilCoalescingPrecedence, SemiringAdditionPrecedence | |
} | |
precedencegroup SemiringAdditionPrecedence { | |
associativity: left | |
} | |
infix operator <+> : SemiringAdditionPrecedence | |
infix operator <*> : SemiringMultiplicationPrecedence | |
infix operator <> : MonoidComposePrecedence | |
// -------------------------------------------------------------------------------- | |
// MARK: - Protocols | |
// -------------------------------------------------------------------------------- | |
protocol Semigroup { | |
static func <> (lhs: Self, rhs: Self) -> Self | |
} | |
protocol Monoid: Semigroup { | |
static var empty: Self { get } | |
} | |
protocol Semiring { | |
static func <+> (lhs: Self, rhs: Self) -> Self | |
static func <*> (lhs: Self, rhs: Self) -> Self | |
static var zero: Self { get } | |
static var one: Self { get } | |
} | |
// -------------------------------------------------------------------------------- | |
// MARK: - Typealias | |
// -------------------------------------------------------------------------------- | |
typealias Endo<A> = (A) -> A | |
typealias AnimationMetaData = TimeInterval | |
// -------------------------------------------------------------------------------- | |
// MARK: - Update | |
// -------------------------------------------------------------------------------- | |
struct Update<T> { | |
let update: Endo<T> | |
@discardableResult | |
func applyTo(_ t: T) -> T { | |
return update(t) | |
} | |
init(_ s: @escaping Endo<T>) { | |
update = s | |
} | |
init<V>(_ k: WritableKeyPath<T, V>, _ v: V) { | |
update = { t in | |
var tt = t | |
tt[keyPath: k] = v | |
return tt | |
} | |
} | |
init<V>(_ k: KeyPath<T, V>, _ v: Update<V>) where V: UIView, T: UIView { | |
update = { t in | |
let tt = t | |
v.applyTo(tt[keyPath: k]) | |
return tt | |
} | |
} | |
init<V>(_ k: WritableKeyPath<T, V>, _ f: @escaping (V) -> V) { | |
update = { t in | |
var tt = t | |
tt[keyPath: k] = f(t[keyPath: k]) | |
return tt | |
} | |
} | |
init<V>(_ k: WritableKeyPath<T, V?>, _ f: @escaping (V) -> V) { | |
update = { t in | |
guard let v = t[keyPath: k] else { return t } | |
var tt = t | |
tt[keyPath: k] = f(v) | |
return tt | |
} | |
} | |
init<V>(_ k: WritableKeyPath<T, V?>, _ f: @escaping (V) -> () -> V) { | |
update = { t in | |
guard let v = t[keyPath: k] else { return t } | |
var tt = t | |
tt[keyPath: k] = f(v)() | |
return tt | |
} | |
} | |
init<V>(_ k: WritableKeyPath<T, V?>, _ f: @escaping () -> V) { | |
update = { t in | |
var tt = t | |
tt[keyPath: k] = f() | |
return tt | |
} | |
} | |
} | |
extension Update: Monoid { | |
static var empty: Update { | |
return Update({ $0 }) | |
} | |
static func <> (lhs: Update, rhs: Update) -> Update { | |
return Update { d in rhs.update(lhs.update(d)) } | |
} | |
} | |
// -------------------------------------------------------------------------------- | |
// MARK: - Operators | |
// -------------------------------------------------------------------------------- | |
func ^ <T, V>(lhs: WritableKeyPath<T, V>, rhs: V) -> Update<T> { | |
return Update(lhs, rhs) | |
} | |
/// This is a special case, we dont need a writable keypath | |
/// to step into a View. It's a reference type. | |
func ^ <T, V>(lhs: KeyPath<T, V>, rhs: Update<V>) -> Update<T> where V: UIView, T: UIView { | |
return Update(lhs, rhs) | |
} | |
func ^ <T, V>(lhs: WritableKeyPath<T, V?>, rhs: @escaping (V) -> V) -> Update<T> { | |
return Update(lhs, rhs) | |
} | |
func ^ <T, V>(lhs: WritableKeyPath<T, V?>, rhs: @escaping (V) -> () -> V) -> Update<T> { | |
return Update(lhs, rhs) | |
} | |
func ^ <T, V>(lhs: WritableKeyPath<T, V?>, rhs: @escaping () -> V) -> Update<T> { | |
return Update(lhs, rhs) | |
} | |
func ^ <T, V>(lhs: WritableKeyPath<T, V>, rhs: @escaping (V) -> V) -> Update<T> { | |
return Update(lhs, rhs) | |
} | |
// -------------------------------------------------------------------------------- | |
// MARK: - Styleable | |
// -------------------------------------------------------------------------------- | |
protocol Styleable: class {} | |
extension UIView: Styleable {} | |
extension Styleable { | |
func style(_ u: Update<Self>, animated: Bool = false) { | |
if animated { | |
UIView.animate(withDuration: 0.1) { [unowned self] in | |
u.applyTo(self) | |
} | |
return | |
} | |
u.applyTo(self) | |
} | |
func animate(_ animation: Animation<Self>, _ callback: (() -> Void)? = nil) { | |
animation.run(on: self, callback) | |
} | |
} | |
// -------------------------------------------------------------------------------- | |
// MARK: - Animation | |
// -------------------------------------------------------------------------------- | |
func step<T: Styleable>(in time: Double = 0.3, _ u: Update<T>) -> Animation<T> { | |
return Animation.step(u, time) | |
} | |
indirect enum Animation<V: Styleable>: Semiring { | |
/// Three cases: | |
/// - no: do nothing | |
/// - of: a single animation | |
/// - ofAnd: a animation and another one afterwards | |
case no | |
case step(Update<V>, AnimationMetaData) | |
case steps(Update<V>, AnimationMetaData, Animation) | |
/// Any animation can be destroyed | |
static var zero: Animation { | |
return .no | |
} | |
/// An nothing animation | |
static var one: Animation { | |
return .step(Update<V>.empty, 0) | |
} | |
/// Run the animation | |
func run(on view: V, _ callback: (() -> Void)? = nil) { | |
UIView.animate(view, self, callback) | |
} | |
/// Get the current Update | |
var current: ((V) -> V)? { | |
switch self { | |
case let .steps(a, _, _), | |
let .step(a, _): | |
return a.update | |
default: | |
return nil | |
} | |
} | |
/// Get the current duration | |
var duration: TimeInterval { | |
switch self { | |
case let .steps(_, t, _), | |
let .step(_, t): | |
return t | |
default: return 0 | |
} | |
} | |
/// Get the next Update | |
var next: Animation? { | |
switch self { | |
case let .steps(_, _, n): | |
return n | |
default: | |
return nil | |
} | |
} | |
} | |
/// Combine 2 Animations in parallel | |
func <+> <T>(lhs: Animation<T>, rhs: Animation<T>) -> Animation<T> { | |
/// a: animation | |
/// t: time | |
/// n: next | |
switch (lhs, rhs) { | |
/// animation a + no = animation a | |
case let (a, .no), let (.no, a): | |
return a | |
/// animation a + animation b = animation (a b) | |
case let (.step(a1, n1), .step(a2, _)): | |
return .step(a1 <> a2, n1) | |
/// animation a ... + animation b = animation (a b) ... | |
case let (.steps(a1, t1, n1), .step(a2, _)), | |
let (.step(a1, t1), .steps(a2, _, n1)): | |
return .steps(a1 <> a2, t1, n1) | |
/// animation a ... + animation b ... = animation (a b) (... ...) | |
case let (.steps(a1, t1, n1), .steps(a2, _, n2)): | |
return .steps(a1 <> a2, t1, n1 <+> n2) | |
} | |
} | |
/// Combine 2 Animations after each other | |
func <*> <T>(lhs: Animation<T>, rhs: Animation<T>) -> Animation<T> { | |
/// a: animation | |
/// t: time | |
/// n: next | |
switch (lhs, rhs) { | |
/// animation a * no = no | |
case (.no, _), (_, .no): | |
return .no | |
/// animation a * animation b = animation (a animation b) | |
case let (.step(a1, t1), .step(a2, t2)): | |
return .steps(a1, t1, .step(a2, t2)) | |
/// animation a ... * animation b = animation (a (... animation b)) | |
case let (.steps(a1, t1, n1), .step(a2, t2)): | |
return .steps(a1, t1, n1 <*> .step(a2, t2)) | |
/// animation a * animation b ... = animation (a (animation b ...)) | |
case let (.step(a1, t1), .steps(a2, t2, n2)): | |
return .steps(a1, t1, .steps(a2, t2, n2)) | |
/// animation a ... * animation b ... = animation (a ... (animation b ...)) | |
case let (.steps(a1, t1, n1), .steps(a2, t2, n2)): | |
return .steps(a1, t1, n1 <*> .steps(a2, t2, n2)) | |
} | |
} | |
extension UIView { | |
/// Animate an animation | |
static func animate<V>(_ view: V, | |
_ animation: Animation<V>, | |
_ callback: (() -> Void)? = nil) { | |
guard let current = animation.current else { | |
callback?() | |
return | |
} | |
let completion: (Bool) -> Void = { _ in | |
guard let next = animation.next else { | |
callback?() | |
return | |
} | |
UIView.animate(view, next, callback) | |
} | |
UIView.animate(withDuration: animation.duration, | |
delay: 0, | |
options: [], | |
animations: { _ = current(view) }, | |
completion: completion) | |
} | |
} | |
// -------------------------------------------------------------------------------- | |
// MARK: - Example | |
// -------------------------------------------------------------------------------- | |
class Controller: UIViewController { | |
let button1 = UIButton(frame: CGRect(x: 20, y: 20, width: 330, height: 100)) | |
let button2 = UIButton(frame: CGRect(x: 20, y: 140, width: 330, height: 100)) | |
let animatedView = UIView(frame: CGRect(x: 20, y: 260, width: 330, height: 100)) | |
let scale: (CGFloat) -> CGAffineTransform = { CGAffineTransform(scaleX: $0, y: $0) } | |
let rotate: (CGFloat) -> CGAffineTransform = { CGAffineTransform(rotationAngle: $0) } | |
lazy var enlarge: Animation<UIView> = | |
step(in: 0.5, \UIView.transform ^ scale(2)) <*> | |
step(in: 2, \.transform ^ rotate(45).concatenating(scale(1.5))) <*> | |
step(in: 2, \.transform ^ rotate(0).concatenating(scale(1.5))) <*> | |
step(in: 0.5, \.transform ^ scale(1)) | |
let colorize: Animation<UIView> = | |
step(\.backgroundColor ^ .blue) <*> | |
.one <*> | |
.one <*> | |
step(\.backgroundColor ^ .black) | |
override func viewDidLoad() { | |
button1.setTitle("enlarge", for: .normal) | |
button1.backgroundColor = .gray | |
button1.addTarget(self, action: #selector(animate1), for: .touchUpInside) | |
button2.setTitle("enlarge <+> colorize", for: .normal) | |
button2.backgroundColor = .gray | |
button2.addTarget(self, action: #selector(animate2), for: .touchUpInside) | |
animatedView.backgroundColor = .black | |
view.addSubview(button1) | |
view.backgroundColor = .white | |
view.addSubview(button2) | |
view.addSubview(animatedView) | |
} | |
@objc func animate1(){ | |
animatedView.animate(enlarge) | |
} | |
@objc func animate2(){ | |
animatedView.animate(enlarge <+> colorize) | |
} | |
} | |
PlaygroundPage.current.liveView = Controller() | |
PlaygroundPage.current.needsIndefiniteExecution = true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment