Skip to content

Instantly share code, notes, and snippets.

@maximkrouk
Last active May 30, 2020 17:05
Show Gist options
  • Save maximkrouk/322e7e8aa50ebd5dd23df9889502732e to your computer and use it in GitHub Desktop.
Save maximkrouk/322e7e8aa50ebd5dd23df9889502732e to your computer and use it in GitHub Desktop.
Type-safe KeyPath driven animations
// Use like [Animation].forEach(run)
public func run<Animation: CAKeyPathDrivenAnimation>(animation: Animation) { animation.run() }
public protocol CAKeyPathDrivenAnimation: CABasicAnimation {
typealias Delegate = CAKeyPathDrivenAnimationDelegate<Layer, Value>
associatedtype Layer: CALayer
associatedtype Value
var _delegate: Delegate? { get }
func run()
}
public class CAInfiniteAnimation<Layer: CALayer, Value>: CABasicAnimation, CAKeyPathDrivenAnimation {
public typealias Delegate = CAKeyPathDrivenAnimationDelegate<Layer, Value>
private(set) public var _delegate: Delegate? {
didSet { delegate = _delegate }
}
public override init() {
super.init()
self.repeatCount = .infinity
self.isCumulative = true
}
public convenience init(on layer: Layer,
_ keyPath: Delegate.Target,
with duration: CFTimeInterval) {
self.init()
self.duration = duration
self._delegate = Delegate(layer: layer, keyPath)
self.delegate = _delegate
self.keyPath = keyPath.asString
}
public convenience init(on layer: Layer,
_ keyPath: Delegate.Target,
from: Value,
to: Value,
with duration: CFTimeInterval) {
self.init(on: layer, keyPath, with: duration)
self.fromValue = from
self.toValue = to
}
public convenience init(on layer: Layer,
_ keyPath: Delegate.Target,
from: Value,
by: Value,
with duration: CFTimeInterval) {
self.init(on: layer, keyPath, with: duration)
self.fromValue = from
self.byValue = by
}
public func run() {
guard let layer = _delegate?.layer, let path = _delegate?.keyPath
else { return }
layer.add(self, forKey: path.asString)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class CAKeyPathDrivenAnimationDelegate<Layer: CALayer, Value>: NSObject, CAAnimationDelegate {
public typealias Target = ReferenceWritableKeyPath<Layer, Value>
public typealias StartAction = (CAAnimation) -> Void
private(set) public weak var layer: Layer?
private(set) public var keyPath: Target
public var shouldUpdateValueOnCompletion: Bool
public var startHandler: ((Layer?, CAAnimation) -> Void)?
public var stopHandler: ((Layer?, CAAnimation, Bool) -> Void)?
public init(layer: Layer,
_ keyPath: Target,
shouldUpdateValueOnCompletion: Bool = true) {
self.layer = layer
self.keyPath = keyPath
self.shouldUpdateValueOnCompletion = shouldUpdateValueOnCompletion
}
public func animationDidStart(_ anim: CAAnimation) {
startHandler?(layer, anim)
}
public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
stopHandler?(layer, anim, flag)
guard
flag,
shouldUpdateValueOnCompletion,
let layer = layer,
let animation = anim as? CABasicAnimation,
let animationValue = animation.toValue as? Value
else { return }
CATransaction.execute { layer[keyPath: keyPath] = animationValue }
}
}
// Applies the last animations state to layer
public class CAMutatingAnimation<Layer: CALayer, Value>: CABasicAnimation, CAKeyPathDrivenAnimation {
private(set) public var _delegate: CAKeyPathDrivenAnimationDelegate<Layer, Value>? {
didSet { delegate = _delegate }
}
public override init() {
super.init()
self.fillMode = .forwards
}
public convenience init(on layer: Layer,
_ keyPath: Delegate.Target,
with duration: CFTimeInterval) {
self.init()
self.duration = duration
self._delegate = Delegate(layer: layer, keyPath)
self.delegate = _delegate
self.keyPath = keyPath.asString
}
public convenience init(on layer: Layer,
_ keyPath: Delegate.Target,
to value: Value,
with duration: CFTimeInterval) {
self.init(on: layer, keyPath, with: duration)
self.toValue = value
}
public convenience init(on layer: Layer,
_ keyPath: Delegate.Target,
by value: Value,
with duration: CFTimeInterval) {
self.init(on: layer, keyPath, with: duration)
self.byValue = value
}
public func run() {
guard let layer = _delegate?.layer, let path = _delegate?.keyPath
else { return }
self.fromValue = layer[keyPath: path]
layer.add(self, forKey: path.asString)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import UIKit
// Objc-like string keypath from a Swifty one
extension AnyKeyPath {
/// Returns key path represented as a string
public var asString: String? {
return _kvcKeyPathString?.description
}
}
// Atomic change
extension CATransaction {
public static func execute(code: () -> Void) {
begin()
setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
code()
commit()
}
}
extension CABasicAnimation {
public convenience init<Layer: CALayer, Value>(keyPath: KeyPath<Layer, Value>) {
self.init(keyPath: keyPath.asString)
}
}
@maximkrouk
Copy link
Author

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