Mission Control Turbo: macOS multitasking turbocharged
1import Foundation
2
3/// Timer-driven critically-damped spring animator.
4/// Runs at ~120Hz on the main queue, driving progress from current value toward a target.
5final class SpringAnimator {
6 private var timer: DispatchSourceTimer?
7 private var current: CGFloat
8 private var velocity: CGFloat
9 private let target: CGFloat
10 private let onUpdate: (CGFloat) -> Void
11 private let onComplete: () -> Void
12
13 // Spring parameters (critically damped)
14 private let stiffness: CGFloat
15 private let damping: CGFloat // computed for critical damping: 2 * sqrt(stiffness)
16
17 init(
18 from: CGFloat,
19 to target: CGFloat,
20 initialVelocity: CGFloat = 0,
21 stiffness: CGFloat = 300.0,
22 onUpdate: @escaping (CGFloat) -> Void,
23 onComplete: @escaping () -> Void
24 ) {
25 self.current = from
26 self.target = target
27 self.velocity = initialVelocity
28 self.stiffness = stiffness
29 self.onUpdate = onUpdate
30 self.onComplete = onComplete
31 self.damping = 2.0 * sqrt(stiffness)
32 }
33
34 func start() {
35 stop()
36 let dt: CGFloat = 1.0 / 120.0
37 let timer = DispatchSource.makeTimerSource(queue: .main)
38 timer.schedule(deadline: .now(), repeating: Double(dt))
39 timer.setEventHandler { [weak self] in
40 self?.tick(dt: dt)
41 }
42 self.timer = timer
43 timer.resume()
44 }
45
46 func stop() {
47 timer?.cancel()
48 timer = nil
49 }
50
51 private func tick(dt: CGFloat) {
52 let displacement = current - target
53 let springForce = -stiffness * displacement
54 let dampingForce = -damping * velocity
55 let acceleration = springForce + dampingForce
56
57 velocity += acceleration * dt
58 current += velocity * dt
59
60 // Settle when close enough
61 if abs(current - target) < 0.001 && abs(velocity) < 0.01 {
62 current = target
63 velocity = 0
64 onUpdate(current)
65 stop()
66 onComplete()
67 return
68 }
69
70 onUpdate(current)
71 }
72}