Skip to content

Instantly share code, notes, and snippets.

@elm4ward
Created November 15, 2017 18:31
Show Gist options
  • Save elm4ward/bf0d6cf3831e54aa6dc83cc3c0b08c69 to your computer and use it in GitHub Desktop.
Save elm4ward/bf0d6cf3831e54aa6dc83cc3c0b08c69 to your computer and use it in GitHub Desktop.
Animations As Semiring
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
@elm4ward
Copy link
Author

elm4ward commented Nov 15, 2017

I am not sure if <+> is actually really the correct implementation - or if its currently a zip operation...
But then - how does <+> look like?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment