Last active
May 29, 2023 14:55
-
-
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.
This file contains 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 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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Using this. I was trying to bridge SwiftUI
Animation
present inUIViewRepresentable
update
method to animate UIKit properly.