Skip to content

Instantly share code, notes, and snippets.

@DevAndArtist
Last active May 29, 2023 14:55
Show Gist options
  • Save DevAndArtist/ebb40a020c216ddcb195fca961b1b515 to your computer and use it in GitHub Desktop.
Save DevAndArtist/ebb40a020c216ddcb195fca961b1b515 to your computer and use it in GitHub Desktop.
This is a custom implementation of some parts of SwiftUI which I could observe.
import CoreGraphics
final class TransactionStack {
static let shared = TransactionStack()
private var _transactions: [Transaction]
private init() {
dispatchPrecondition(condition: .onQueue(.main))
_transactions = []
}
// The system can peek into the stack and obtain a transaction to produce
// implicit animations during the next layout pass.
//
// I suppose that we should peek for every view that we want to animate
// each mutation we made during the `body` call. It's important that we
// peek during the side-effect of the `body` call because after that
// the `withTransaction` free function will pop the transaction from the
// stack.
func peek() -> Transaction? {
dispatchPrecondition(condition: .onQueue(.main))
return _transactions.last
}
}
// Those two methods are file private in order to allow only `withTransaction`
// function to push and pop transactions.
extension TransactionStack {
fileprivate func push(_ transaction: Transaction) {
dispatchPrecondition(condition: .onQueue(.main))
_transactions.append(transaction)
}
@discardableResult
fileprivate func pop() -> Transaction? {
dispatchPrecondition(condition: .onQueue(.main))
return _transactions.popLast()
}
}
public func withTransaction<Result>(
_ transaction: Transaction,
_ body: () throws -> Result
) rethrows -> Result {
dispatchPrecondition(condition: .onQueue(.main))
let stack = TransactionStack.shared
stack.push(transaction)
// We use defer so that the transaction is popped from the stack on success
// or on failure of the `body` call.
defer {
assert(stack.pop() != nil, "desynchronized stack")
}
return try body()
}
public func withAnimation<Result>(
_ animation: Animation? = .default,
_ body: () throws -> Result
) rethrows -> Result {
// Not sure if we should create a new transaction, or rather peek into the stack
// make a copy of the transaction from the stack, mutate its animation and
// forward that to `withTransaction`.
//
// ```swift
// var transaction = Transaction(animation: .default)
// transaction.disablesAnimations = true
// withTransaction(transaction) {
// withAnimation(.spring()) {
// /* perform actions */
//
// // does the nested transaction have `disablesAnimations` set
// // to `true` in real SwiftUI at this point?
// }
// }
// ```
let transaction = Transaction(animation: animation)
return try withTransaction(transaction, body)
}
public struct Transaction {
public var animation: Animation?
public var disablesAnimations: Bool
public init(animation: Animation?) {
self.animation = animation
self.disablesAnimations = false
}
public init() {
self.init(animation: .none)
}
}
struct BezierTimingCurve: Equatable {
let ax: Double
let bx: Double
let cx: Double
let ay: Double
let by: Double
let cy: Double
}
// Custom protocol to anchor the types.
// Not sure if needed at all. If it's needed, does this protocol
// provide us any more functionality.
protocol _Animation: Equatable {}
// Existential box type?
struct AnyAnimator: _Animation {
init<Animation>(animation: Animation) where Animation: _Animation {
// Not yet sure how to implement this box.
}
}
struct BezierAnimation: _Animation {
let duration: Double
let curve: BezierTimingCurve
}
struct FluidSpringAnimation: _Animation {
let response: Double
let dampingFraction: Double
let blendDuration: Double
}
struct SpringAnimation: _Animation {
let mass: Double
let stiffness: Double
let damping: Double
let initialVelocity: _Velocity<Double>
}
struct SpeedAnimation<Animation>: _Animation where Animation: _Animation {
let animation: Animation
let speed: Double
}
struct DelayAnimation<Animation>: _Animation where Animation: _Animation {
let animation: Animation
let delay: Double
}
struct RepeatAnimation<Animation>: _Animation where Animation: _Animation {
let animation: Animation
let repeatCount: Int?
let autoreverses: Bool
}
public struct _Velocity<Value>: Equatable where Value: Equatable {
public var valuePerSecond: Value
public init(valuePerSecond: Value) {
self.valuePerSecond = valuePerSecond
}
}
extension _Velocity: Comparable where Value: Comparable {
public static func < (lhs: _Velocity<Value>, rhs: _Velocity<Value>) -> Bool {
return lhs.valuePerSecond < rhs.valuePerSecond
}
}
extension _Velocity : AdditiveArithmetic where Value : AdditiveArithmetic {
public init() {
self.init(valuePerSecond: .zero)
}
public static var zero: _Velocity {
return _Velocity(valuePerSecond: .zero)
}
public static func += (lhs: inout _Velocity, rhs: _Velocity) {
lhs.valuePerSecond += rhs.valuePerSecond
}
public static func -= (lhs: inout _Velocity, rhs: _Velocity) {
lhs.valuePerSecond -= rhs.valuePerSecond
}
public static func + (lhs: _Velocity, rhs: _Velocity) -> _Velocity {
var velocity = lhs;
velocity += rhs
return velocity
}
public static func - (lhs: _Velocity, rhs: _Velocity) -> _Velocity {
var velocity = lhs
velocity -= rhs
return velocity
}
}
extension _Velocity : VectorArithmetic where Value : VectorArithmetic {
public mutating func scale(by rhs: Double) {
valuePerSecond.scale(by: rhs)
}
public var magnitudeSquared: Double {
return valuePerSecond.magnitudeSquared
}
}
public protocol VectorArithmetic: AdditiveArithmetic {
mutating func scale(by rhs: Double)
var magnitudeSquared: Double { get }
}
extension Float: VectorArithmetic {
public mutating func scale(by rhs: Double) {
self *= Float(rhs)
}
public var magnitudeSquared: Double {
return Double(self * self)
}
}
extension Double: VectorArithmetic {
public mutating func scale(by rhs: Double) {
self *= rhs
}
public var magnitudeSquared: Double {
return self * self
}
}
extension CGFloat: VectorArithmetic {
public mutating func scale(by rhs: Double) {
self *= CGFloat(rhs)
}
public var magnitudeSquared: Double {
return Double(self * self)
}
}
public struct Animation: Equatable {
let base: AnyAnimator
fileprivate init(base: AnyAnimator) {
self.base = base
}
public func delay(_ delay: Double) -> Animation {
let animation = DelayAnimation(animation: base, delay: delay)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public func speed(_ speed: Double) -> Animation {
let animation = SpeedAnimation(animation: base, speed: speed)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public func repeatCount(
_ repeatCount: Int,
autoreverses: Bool = true
) -> Animation {
let animation = RepeatAnimation(
animation: base,
repeatCount: repeatCount,
autoreverses: autoreverses
)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public func repeatForever(autoreverses: Bool = true) -> Animation {
let animation = RepeatAnimation(
animation: base,
repeatCount: .none,
autoreverses: autoreverses
)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
}
extension Animation {
public static let `default`: Animation = .easeInOut
public static func easeInOut(duration: Double) -> Animation {
let curve = BezierTimingCurve(
ax: 0.52,
bx: -0.78,
cx: 1.26,
ay: -2.0,
by: 3.0,
cy: 0.0
)
let animation = BezierAnimation(duration: duration, curve: curve)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public static var easeInOut: Animation {
return easeInOut(duration: 0.35)
}
public static func easeIn(duration: Double) -> Animation {
let curve = BezierTimingCurve(
ax: -0.7400000000000002,
bx: 0.4800000000000002,
cx: 1.26,
ay: -2.0,
by: 3.0,
cy: 0.0
)
let animation = BezierAnimation(duration: duration, curve: curve)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public static var easeIn: Animation {
return easeIn(duration: 0.35)
}
public static func easeOut(duration: Double) -> Animation {
let curve = BezierTimingCurve(
ax: -0.7399999999999998,
bx: 1.7399999999999998,
cx: 0.0,
ay: -2.0,
by: 3.0,
cy: 0.0
)
let animation = BezierAnimation(duration: duration, curve: curve)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public static var easeOut: Animation {
return easeOut(duration: 0.35)
}
public static func linear(duration: Double) -> Animation {
let curve = BezierTimingCurve(
ax: -2.0,
bx: 3.0,
cx: 0.0,
ay: -2.0,
by: 3.0,
cy: 0.0
)
let animation = BezierAnimation(duration: duration, curve: curve)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public static var linear: Animation {
return linear(duration: 0.35)
}
public static func timingCurve(
_ c0x: Double,
_ c0y: Double,
_ c1x: Double,
_ c1y: Double,
duration: Double = 0.35
) -> Animation {
// This may help: https://pomax.github.io/bezierinfo/
fatalError("no idea how to map the control points to the matrix")
}
public static func interpolatingSpring(
mass: Double = 1.0,
stiffness: Double,
damping: Double,
initialVelocity: Double = 0.0
) -> Animation {
let animation = SpringAnimation(
mass: mass,
stiffness: stiffness,
damping: damping,
initialVelocity: _Velocity(valuePerSecond: initialVelocity)
)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public static func spring(
response: Double = 0.55,
dampingFraction: Double = 0.825,
blendDuration: Double = 0
) -> Animation {
let animation = FluidSpringAnimation(
response: response,
dampingFraction: dampingFraction,
blendDuration: blendDuration
)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
public static func interactiveSpring(
response: Double = 0.15,
dampingFraction: Double = 0.86,
blendDuration: Double = 0.25
) -> Animation {
let animation = FluidSpringAnimation(
response: response,
dampingFraction: dampingFraction,
blendDuration: blendDuration
)
let base = AnyAnimator(animation: animation)
return Animation(base: base)
}
}
@DevAndArtist
Copy link
Author

DevAndArtist commented Jul 28, 2019

This basically allows to implement this code:

public struct Binding<Value> {
  public var transaction = Transaction()
  
  private let _getter: () -> Value
  private let _setter: (Value, Transaction) -> Void
  
  public var wrappedValue: Value {
    get {
      // FIXME: Remove `return` in Swift 5.1.
      return _getter()
    }
    nonmutating set {
      _setter(newValue, transaction)
    }
  }
  
  public init(
    getValue getter: @escaping () -> Value,
    setValue setter: @escaping (Value, Transaction) -> Void
  ) {
    _getter = getter
    _setter = setter
  }
  
  public init(
    getValue getter: @escaping () -> Value,
    setValue setter: @escaping (Value) -> Void
  ) {
    self.init(
      getValue: getter,
      setValue: { newValue, transaction in
        // Wrap into the transaction closure but don't call
        // `withTransaction` here. It's up to the user or other
        // API to decide. (e.g. binding returned by `State` does
        // inject `withTransaction` into the setter. 
        setter(newValue)
      }
    )
  }
}

extension BindingConvertible {
  /// Create a new Binding that will apply `transaction` to any changes.
  public func transaction(_ transaction: Transaction) -> Binding<Self.Value> {
    // Override the full transaction, don't inject `withTransaction`.
    var binding = self.binding
    binding.transaction = transaction
    return binding
  }

  /// Create a new Binding that will apply `animation` to any changes.
  public func animation(
    _ animation: Animation? = .default
  ) -> Binding<Self.Value> {
    // We do not override the full transaction, just the animation,
    // don't inject `withTransaction`
    var binding = self.binding
    binding.transaction.animation = animation
    return binding
  }
}

@lorenzofiamingo
Copy link

Using this. I was trying to bridge SwiftUI Animation present in UIViewRepresentable update method to animate UIKit properly.

  public static func timingCurve(
    _ c0x: Double,
    _ c0y: Double,
    _ c1x: Double,
    _ c1y: Double,
    duration: Double = 0.35
  ) -> Animation {
    let curve = BezierTimingCurve(
      ax: 3*c0x-3*c1x+1,
      bx: -6*c0x+3*c1x,
      cx: 3*c0x,
      ay: 3*c0y-3*c1y+1,
      by: -6*c0y+3*c1y,
      cy: 3*c0y
    )
    let animation = BezierAnimation(duration: duration, curve: curve)
    let base = AnyAnimator(animation: animation)
    return Animation(base: base)
  }

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