import Foundation import UIKit import AsyncDisplayKit import ObjCRuntimeUtils extension CGRect { var center: CGPoint { return CGPoint(x: self.midX, y: self.midY) } } public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case linear case easeInOut case spring case customSpring(damping: CGFloat, initialVelocity: CGFloat) case custom(Float, Float, Float, Float) public static var slide: ContainedViewLayoutTransitionCurve { return .custom(0.33, 0.52, 0.25, 0.99) } } public extension ContainedViewLayoutTransitionCurve { func solve(at offset: CGFloat) -> CGFloat { switch self { case .linear: return offset case .easeInOut: return listViewAnimationCurveEaseInOut(offset) case .spring: return listViewAnimationCurveSystem(offset) case .customSpring: return listViewAnimationCurveSystem(offset) case let .custom(c1x, c1y, c2x, c2y): return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset) } } } public extension ContainedViewLayoutTransitionCurve { var timingFunction: String { switch self { case .linear: return CAMediaTimingFunctionName.linear.rawValue case .easeInOut: return CAMediaTimingFunctionName.easeInEaseOut.rawValue case .spring: return kCAMediaTimingFunctionSpring case let .customSpring(damping, initialVelocity): return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)" case .custom: return CAMediaTimingFunctionName.easeInEaseOut.rawValue } } var mediaTimingFunction: CAMediaTimingFunction? { switch self { case .linear: return nil case .easeInOut: return nil case .spring: return nil case .customSpring: return nil case let .custom(p1, p2, p3, p4): return CAMediaTimingFunction(controlPoints: p1, p2, p3, p4) } } var viewAnimationOptions: UIView.AnimationOptions { switch self { case .linear: return [.curveLinear] case .easeInOut: return [.curveEaseInOut] case .spring: return UIView.AnimationOptions(rawValue: 7 << 16) case .customSpring: return UIView.AnimationOptions(rawValue: 7 << 16) case .custom: return [] } } } public enum ContainedViewLayoutTransition { case immediate case animated(duration: Double, curve: ContainedViewLayoutTransitionCurve) public var isAnimated: Bool { if case .immediate = self { return false } else { return true } } } public extension CGRect { var ensuredValid: CGRect { if !ASIsCGRectValidForLayout(CGRect(origin: CGPoint(), size: self.size)) { return CGRect() } if !ASIsCGPositionValidForLayout(self.origin) { return CGRect() } return self } } private extension CALayer { func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: ContainedViewLayoutTransitionCurve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) { let timingFunction: String let mediaTimingFunction: CAMediaTimingFunction? switch curve { case .spring: timingFunction = kCAMediaTimingFunctionSpring mediaTimingFunction = nil default: timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue mediaTimingFunction = curve.mediaTimingFunction } self.animate( from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion ) } } public extension ContainedViewLayoutTransition { func animation() -> CABasicAnimation? { switch self { case .immediate: return nil case let .animated(duration, curve): let animation = CALayer().makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "position", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: false, completion: { _ in }) return animation as? CABasicAnimation } } func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if frame.origin.x.isNaN { return } if frame.origin.y.isNaN { return } if frame.size.width.isNaN { return } if frame.size.width < 0.0 { return } if frame.size.height.isNaN { return } if frame.size.height < 0.0 { return } if !ASIsCGRectValidForLayout(CGRect(origin: CGPoint(), size: frame.size)) { return } if !ASIsCGPositionValidForLayout(frame.origin) { return } if node.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: node.layer.removeAnimation(forKey: "position") node.layer.removeAnimation(forKey: "bounds") node.frame = frame if let completion = completion { completion(true) } case let .animated(duration, curve): let previousFrame: CGRect if beginWithCurrentState, (node.layer.animation(forKey: "position") != nil || node.layer.animation(forKey: "bounds") != nil), let presentation = node.layer.presentation() { previousFrame = presentation.frame } else { previousFrame = node.frame } node.frame = frame node.layer.animateFrame(from: previousFrame, to: frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } }) } } } func updateFrameAsPositionAndBounds(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: node.layer.removeAnimation(forKey: "position") node.layer.removeAnimation(forKey: "bounds") node.position = frame.center node.bounds = CGRect(origin: CGPoint(), size: frame.size) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousPosition: CGPoint let previousBounds: CGRect if beginWithCurrentState, let presentation = node.layer.presentation() { previousPosition = presentation.position previousBounds = presentation.bounds } else { previousPosition = node.position previousBounds = node.bounds } node.position = frame.center node.bounds = CGRect(origin: CGPoint(), size: frame.size) node.layer.animateFrame(from: CGRect(origin: CGPoint(x: previousPosition.x - previousBounds.width / 2.0, y: previousPosition.y - previousBounds.height / 2.0), size: previousBounds.size), to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } }) } } } func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: layer.removeAnimation(forKey: "position") layer.removeAnimation(forKey: "bounds") layer.position = frame.center layer.bounds = CGRect(origin: CGPoint(), size: frame.size) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousPosition: CGPoint let previousBounds: CGRect if beginWithCurrentState, let presentation = layer.presentation() { previousPosition = presentation.position previousBounds = presentation.bounds } else { previousPosition = layer.position previousBounds = layer.bounds } layer.position = frame.center layer.bounds = CGRect(origin: CGPoint(), size: frame.size) layer.animateFrame(from: CGRect(origin: CGPoint(x: previousPosition.x - previousBounds.width / 2.0, y: previousPosition.y - previousBounds.height / 2.0), size: previousBounds.size), to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } }) } } } func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: node.frame = frame if let completion = completion { completion(true) } case .animated: let previousFrame = node.frame node.frame = frame self.animatePositionAdditive(node: node, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY)) } } } func updateFrameAdditive(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if view.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: view.frame = frame if let completion = completion { completion(true) } case .animated: let previousFrame = view.frame view.frame = frame self.animatePositionAdditive(layer: view.layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY)) } } } func updateFrameAdditiveToCenter(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: node.position = frame.center node.bounds = CGRect(origin: node.bounds.origin, size: frame.size) if let completion = completion { completion(true) } case .animated: let previousCenter = node.frame.center node.position = frame.center node.bounds = CGRect(origin: node.bounds.origin, size: frame.size) self.animatePositionAdditive(node: node, offset: CGPoint(x: previousCenter.x - frame.midX, y: previousCenter.y - frame.midY)) } } } func updateFrameAdditiveToCenter(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if view.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: view.center = frame.center view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) if let completion = completion { completion(true) } case .animated: let previousCenter = view.frame.center view.center = frame.center view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) self.animatePositionAdditive(layer: view.layer, offset: CGPoint(x: previousCenter.x - frame.midX, y: previousCenter.y - frame.midY)) } } } func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) } else { switch self { case .immediate: node.layer.removeAnimation(forKey: "bounds") node.bounds = bounds if let completion = completion { completion(true) } case let .animated(duration, curve): let previousBounds: CGRect if beginWithCurrentState, node.layer.animation(forKey: "bounds") != nil, let presentation = node.layer.presentation() { previousBounds = presentation.bounds } else { previousBounds = node.bounds } node.bounds = bounds node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } }) } } } func updateBounds(layer: CALayer, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.bounds.equalTo(bounds) && !force { completion?(true) } else { switch self { case .immediate: layer.removeAnimation(forKey: "bounds") layer.bounds = bounds if let completion = completion { completion(true) } case let .animated(duration, curve): let previousBounds = layer.bounds layer.bounds = bounds layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } }) } } } func updatePosition(node: ASDisplayNode, position: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.position.equalTo(position) { completion?(true) } else { switch self { case .immediate: node.layer.removeAnimation(forKey: "position") node.position = position if let completion = completion { completion(true) } case let .animated(duration, curve): let previousPosition: CGPoint if beginWithCurrentState, node.layer.animation(forKey: "position") != nil, let presentation = node.layer.presentation() { previousPosition = presentation.position } else { previousPosition = node.position } node.position = position node.layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } } func updatePosition(layer: CALayer, position: CGPoint, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.position.equalTo(position) && !force { completion?(true) } else { switch self { case .immediate: layer.removeAnimation(forKey: "position") layer.position = position if let completion = completion { completion(true) } case let .animated(duration, curve): let previousPosition = layer.position layer.position = position layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } } func updateAnchorPoint(layer: CALayer, anchorPoint: CGPoint, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.anchorPoint.equalTo(anchorPoint) && !force { completion?(true) } else { switch self { case .immediate: layer.removeAnimation(forKey: "anchorPoint") layer.anchorPoint = anchorPoint if let completion = completion { completion(true) } case let .animated(duration, curve): let previousAnchorPoint = layer.anchorPoint layer.anchorPoint = anchorPoint layer.animateAnchorPoint(from: previousAnchorPoint, to: anchorPoint, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } } func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): layer.animatePosition(from: fromValue, to: toValue, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { completion(result) } }) } } func animatePosition(node: ASDisplayNode, from position: CGPoint, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func animatePosition(node: ASDisplayNode, to position: CGPoint, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { if !additive && node.position.equalTo(position) { completion?(true) } else { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): node.layer.animatePosition(from: additive ? CGPoint() : node.position, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { completion(result) } }) } } } func animatePositionWithKeyframes(node: ASDisplayNode, keyframes: [CGPoint], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { self.animatePositionWithKeyframes(layer: node.layer, keyframes: keyframes, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } func animatePositionWithKeyframes(layer: CALayer, keyframes: [CGPoint], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: completion?(true) case let .animated(duration, curve): layer.animateKeyframes(values: keyframes.map(NSValue.init(cgPoint:)), duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in completion?(value) }) } } func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): node.layer.animateFrame(from: frame, to: toFrame ?? node.layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { completion(result) } }) } } func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { completion(result) } }) } } func animateBounds(layer: CALayer, from bounds: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): layer.animateBounds(from: bounds, to: layer.bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in if let completion = completion { completion(result) } }) } } func animateWidthAdditive(layer: CALayer, value: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): layer.animateWidth(from: value, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { result in if let completion = completion { completion(result) } }) } } func animateHeightAdditive(layer: CALayer, value: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): layer.animateHeight(from: value, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { result in if let completion = completion { completion(result) } }) } } func animateOffsetAdditive(node: ASDisplayNode, offset: CGFloat) { switch self { case .immediate: break case let .animated(duration, curve): node.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) } } func animateHorizontalOffsetAdditive(node: ASDisplayNode, offset: CGFloat, completion: (() -> Void)? = nil) { switch self { case .immediate: break case let .animated(duration, curve): node.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in completion?() }) } } func animateHorizontalOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) { switch self { case .immediate: break case let .animated(duration, curve): layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in completion?() }) } } func animateOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) { switch self { case .immediate: completion?() case let .animated(duration, curve): layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in completion?() }) } } func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { switch self { case .immediate: completion(true) case let .animated(duration, curve): node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } } func animatePositionAdditive(layer: CALayer, offset: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { switch self { case .immediate: completion(true) case let .animated(duration, curve): layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } } func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { switch self { case .immediate: completion?() case let .animated(duration, curve): node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in completion?() }) } } func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: completion?(true) case let .animated(duration, curve): layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { result in completion?(result) }) } } func animateContentsRectPositionAdditive(layer: CALayer, offset: CGPoint, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: completion?(true) case let .animated(duration, curve): layer.animate(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.origin", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } } func updateFrame(view: UIView, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if frame.origin.x.isNaN { return } if frame.origin.y.isNaN { return } if frame.size.width.isNaN { return } if frame.size.width < 0.0 { return } if frame.size.height.isNaN { return } if frame.size.height < 0.0 { return } if !ASIsCGRectValidForLayout(CGRect(origin: CGPoint(), size: frame.size)) { return } if !ASIsCGPositionValidForLayout(frame.origin) { return } if view.frame.equalTo(frame) && !force { completion?(true) } else { switch self { case .immediate: //view.layer.removeAnimation(forKey: "position") //view.layer.removeAnimation(forKey: "bounds") view.frame = frame if let completion = completion { completion(true) } case let .animated(duration, curve): let previousFrame: CGRect if beginWithCurrentState, (view.layer.animation(forKey: "position") != nil || view.layer.animation(forKey: "bounds") != nil), let presentation = view.layer.presentation() { previousFrame = presentation.frame } else { previousFrame = view.frame } view.frame = frame view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } }) } } } func updateFrame(layer: CALayer, frame: CGRect, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if layer.frame.equalTo(frame) { completion?(true) } else { switch self { case .immediate: layer.removeAnimation(forKey: "position") layer.removeAnimation(forKey: "bounds") if let view = layer.delegate as? UIView { view.frame = frame } else { layer.frame = frame } if let completion = completion { completion(true) } case let .animated(duration, curve): let previousFrame: CGRect if beginWithCurrentState, (layer.animation(forKey: "position") != nil || layer.animation(forKey: "bounds") != nil), let presentation = layer.presentation() { previousFrame = presentation.frame } else { previousFrame = layer.frame } layer.frame = frame layer.animateFrame(from: previousFrame, to: frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } } func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, force: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if node.alpha.isEqual(to: alpha) && !force { if let completion = completion { completion(true) } return } switch self { case .immediate: node.alpha = alpha if let completion = completion { completion(true) } case let .animated(duration, curve): let previousAlpha: CGFloat if beginWithCurrentState, let presentation = node.layer.presentation() { previousAlpha = CGFloat(presentation.opacity) } else { previousAlpha = node.alpha } node.alpha = alpha node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateAlpha(layer: CALayer, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.opacity.isEqual(to: Float(alpha)) { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.opacity = Float(alpha) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousAlpha: Float if beginWithCurrentState, let presentation = layer.presentation() { previousAlpha = presentation.opacity } else { previousAlpha = layer.opacity } layer.opacity = Float(alpha) layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateBackgroundColor(node: ASDisplayNode, color: UIColor, completion: ((Bool) -> Void)? = nil) { if let nodeColor = node.backgroundColor, nodeColor.isEqual(color) { if let completion = completion { completion(true) } return } switch self { case .immediate: node.backgroundColor = color if let completion = completion { completion(true) } case let .animated(duration, curve): if let nodeColor = node.backgroundColor { node.backgroundColor = color node.layer.animate(from: nodeColor.cgColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } else { node.backgroundColor = color if let completion = completion { completion(true) } } } } func updateBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { if let nodeColor = layer.backgroundColor, nodeColor == color.cgColor { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.backgroundColor = color.cgColor if let completion = completion { completion(true) } case let .animated(duration, curve): if let nodeColor = layer.backgroundColor { layer.backgroundColor = color.cgColor layer.animate(from: nodeColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } else { layer.backgroundColor = color.cgColor if let completion = completion { completion(true) } } } } func updateCornerRadius(node: ASDisplayNode, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) { if node.cornerRadius.isEqual(to: cornerRadius) { if let completion = completion { completion(true) } return } switch self { case .immediate: node.layer.removeAnimation(forKey: "cornerRadius") node.cornerRadius = cornerRadius if let completion = completion { completion(true) } case let .animated(duration, curve): let previousCornerRadius = node.cornerRadius node.cornerRadius = cornerRadius node.layer.animate(from: NSNumber(value: Float(previousCornerRadius)), to: NSNumber(value: Float(cornerRadius)), keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) { if layer.cornerRadius.isEqual(to: cornerRadius) { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.removeAnimation(forKey: "cornerRadius") layer.cornerRadius = cornerRadius if let completion = completion { completion(true) } case let .animated(duration, curve): let previousCornerRadius = layer.cornerRadius layer.cornerRadius = cornerRadius layer.animate(from: NSNumber(value: Float(previousCornerRadius)), to: NSNumber(value: Float(cornerRadius)), keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateTintColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { if let current = layer.layerTintColor, current == color.cgColor { completion?(true) return } switch self { case .immediate: layer.layerTintColor = color.cgColor completion?(true) case let .animated(duration, curve): let previousColor: CGColor = layer.layerTintColor ?? UIColor.clear.cgColor layer.layerTintColor = color.cgColor layer.animate( from: previousColor, to: color.cgColor, keyPath: "contentsMultiplyColor", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: completion ) } } func updateContentsRect(layer: CALayer, contentsRect: CGRect, completion: ((Bool) -> Void)? = nil) { if layer.contentsRect == contentsRect { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.contentsRect = contentsRect if let completion = completion { completion(true) } case let .animated(duration, curve): let previousContentsRect = layer.contentsRect layer.contentsRect = contentsRect layer.animate(from: NSValue(cgRect: previousContentsRect), to: NSValue(cgRect: contentsRect), keyPath: "contentsRect", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: fromScale) { if let completion = completion { completion(true) } return } switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): let calculatedFrom: CGFloat let calculatedTo: CGFloat if additive { calculatedFrom = fromScale - currentScale calculatedTo = 0.0 } else { calculatedFrom = fromScale calculatedTo = currentScale } node.layer.animateScale(from: calculatedFrom, to: calculatedTo, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, additive: additive, completion: { result in if let completion = completion { completion(result) } }) } } func animateTransformScale(node: ASDisplayNode, from fromScale: CGPoint, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): let calculatedFrom: CGPoint let calculatedTo: CGPoint calculatedFrom = fromScale calculatedTo = CGPoint(x: 1.0, y: 1.0) node.layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) node.layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) } } func animateTransformScale(layer: CALayer, from fromScale: CGPoint, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): let calculatedFrom: CGPoint let calculatedTo: CGPoint calculatedFrom = fromScale calculatedTo = CGPoint(x: 1.0, y: 1.0) layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) } } func animateTransformScale(layer: CALayer, from fromScale: CGPoint, to toScale: CGPoint, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): let calculatedFrom: CGPoint let calculatedTo: CGPoint calculatedFrom = fromScale calculatedTo = toScale layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) } } func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { let t = view.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: fromScale) { if let completion = completion { completion(true) } return } switch self { case .immediate: if let completion = completion { completion(true) } case let .animated(duration, curve): view.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateTransform(node: ASDisplayNode, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion) } func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let transform = CATransform3DMakeAffineTransform(transform) if CATransform3DEqualToTransform(layer.transform, transform) { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.transform = transform if let completion = completion { completion(true) } case let .animated(duration, curve): let previousTransform: CATransform3D if beginWithCurrentState, let presentation = layer.presentation() { previousTransform = presentation.transform } else { previousTransform = layer.transform } layer.transform = transform layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in completion?(value) }) } } func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: scale) { if let completion = completion { completion(true) } return } switch self { case .immediate: node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousScale: CGFloat if beginWithCurrentState, let presentation = node.layer.presentation() { let t = presentation.transform previousScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) } else { previousScale = currentScale } node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) node.layer.animateScale(from: previousScale, to: scale, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateTransformScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { let t = layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: scale) { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.transform = CATransform3DMakeScale(scale, scale, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): layer.transform = CATransform3DMakeScale(scale, scale, 1.0) layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, delay: Double = 0.0, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) completion?(true) return } self.updateSublayerTransformScale(layer: node.layer, scale: CGPoint(x: scale, y: scale), beginWithCurrentState: beginWithCurrentState, completion: completion) } func updateSublayerTransformScaleAdditive(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) completion?(true) return } let t = node.layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: scale) { if let completion = completion { completion(true) } return } switch self { case .immediate: node.layer.removeAnimation(forKey: "sublayerTransform") node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): let t = node.layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) node.layer.animate(from: -(scale - currentScale) as NSNumber, to: 0.0 as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: true, completion: { result in if let completion = completion { completion(result) } }) } } func updateSublayerTransformScaleAndOffset(layer: CALayer, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) let currentOffset = CGPoint(x: t.m41 / currentScale, y: t.m42 / currentScale) if abs(currentScale - scale) <= CGFloat.ulpOfOne && abs(currentOffset.x - offset.x) <= CGFloat.ulpOfOne && abs(currentOffset.y - offset.y) <= CGFloat.ulpOfOne { if let completion = completion { completion(true) } return } let transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x, offset.y, 0.0) switch self { case .immediate: layer.removeAnimation(forKey: "sublayerTransform") layer.sublayerTransform = transform if let completion = completion { completion(true) } case let .animated(duration, curve): let initialTransform: CATransform3D if beginWithCurrentState { initialTransform = layer.presentation()?.sublayerTransform ?? t } else { initialTransform = t } layer.sublayerTransform = transform layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) } }) } } func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) completion?(true) return } return updateSublayerTransformScaleAndOffset(layer: node.layer, scale: scale, offset: offset, beginWithCurrentState: beginWithCurrentState, completion: completion) } func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) completion?(true) return } self.updateSublayerTransformScale(layer: node.layer, scale: scale, beginWithCurrentState: beginWithCurrentState, completion: completion) } func updateSublayerTransformScale(layer: CALayer, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = layer.sublayerTransform let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23)) if t.m22 < 0.0 { currentScaleY = -currentScaleY } if CGPoint(x: currentScaleX, y: currentScaleY) == scale { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.removeAnimation(forKey: "sublayerTransform") layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): let initialTransform: CATransform3D if beginWithCurrentState { initialTransform = layer.presentation()?.sublayerTransform ?? t } else { initialTransform = t } layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) } }) } } func updateTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) completion?(true) return } self.updateTransformScale(layer: node.layer, scale: scale, completion: completion) } func updateTransformScale(layer: CALayer, scale: CGPoint, completion: ((Bool) -> Void)? = nil) { let t = layer.transform let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23)) if t.m22 < 0.0 { currentScaleY = -currentScaleY } if CGPoint(x: currentScaleX, y: currentScaleY) == scale { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.removeAnimation(forKey: "transform") layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) } }) } } func updateSublayerTransformOffset(layer: CALayer, offset: CGPoint, completion: ((Bool) -> Void)? = nil) { let t = layer.sublayerTransform let currentOffset = CGPoint(x: t.m41, y: t.m42) if currentOffset == offset { if let completion = completion { completion(true) } return } switch self { case .immediate: layer.removeAnimation(forKey: "sublayerTransform") layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) if let completion = completion { completion(true) } case let .animated(duration, curve): layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) } }) } } func updateTransformRotation(node: ASDisplayNode, angle: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentAngle = atan2(t.m12, t.m11) if currentAngle.isEqual(to: angle) { if let completion = completion { completion(true) } return } switch self { case .immediate: node.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousAngle: CGFloat if beginWithCurrentState, let presentation = node.layer.presentation() { let t = presentation.transform previousAngle = atan2(t.m12, t.m11) } else { previousAngle = currentAngle } node.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) node.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateTransformRotation(view: UIView, angle: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = view.layer.transform let currentAngle = atan2(t.m12, t.m11) if currentAngle.isEqual(to: angle) { if let completion = completion { completion(true) } return } switch self { case .immediate: view.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousAngle: CGFloat if beginWithCurrentState, let presentation = view.layer.presentation() { let t = presentation.transform previousAngle = atan2(t.m12, t.m11) } else { previousAngle = currentAngle } view.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) view.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updateTransformRotationAndScale(view: UIView, angle: CGFloat, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = view.layer.transform let currentAngle = atan2(t.m12, t.m11) let currentScale = CGPoint(x: t.m11, y: t.m12) if currentAngle.isEqual(to: angle) && currentScale == scale { if let completion = completion { completion(true) } return } switch self { case .immediate: view.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale.x, scale.y, 1.0), angle, 0.0, 0.0, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): let previousAngle: CGFloat if beginWithCurrentState, let presentation = view.layer.presentation() { let t = presentation.transform previousAngle = atan2(t.m12, t.m11) } else { previousAngle = currentAngle } view.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale.x, scale.y, 1.0), angle, 0.0, 0.0, 1.0) view.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } }) } } func updatePath(layer: CAShapeLayer, path: CGPath, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if layer.path == path { completion?(true) return } switch self { case .immediate: layer.removeAnimation(forKey: "path") layer.path = path if let completion = completion { completion(true) } case let .animated(duration, curve): let fromPath = layer.path layer.path = path layer.animate(from: fromPath, to: path, keyPath: "path", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) } }) } } func attachAnimation(view: UIView, id: String, completion: @escaping (Bool) -> Void) { switch self { case .immediate: completion(true) case let .animated(duration, curve): view.layer.animate( from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: id, duration: duration, delay: 0.0, curve: curve, removeOnCompletion: true, additive: false, completion: completion ) } } } public struct CombinedTransition { public var horizontal: ContainedViewLayoutTransition public var vertical: ContainedViewLayoutTransition public var isAnimated: Bool { return self.horizontal.isAnimated || self.vertical.isAnimated } public init(horizontal: ContainedViewLayoutTransition, vertical: ContainedViewLayoutTransition) { self.horizontal = horizontal self.vertical = vertical } public func animateFrame(layer: CALayer, from fromFrame: CGRect, completion: ((Bool) -> Void)? = nil) { //self.horizontal.animateFrame(layer: layer, from: fromFrame, completion: completion) //return; let toFrame = layer.frame enum Keys: CaseIterable { case positionX, positionY case sizeWidth, sizeHeight } var remainingKeys = Keys.allCases var completedValue = true let completeKey: (Keys, Bool) -> Void = { key, completed in remainingKeys.removeAll(where: { $0 == key }) if !completed { completedValue = false } if remainingKeys.isEmpty { completion?(completedValue) } } self.horizontal.animatePositionAdditive(layer: layer, offset: CGPoint(x: fromFrame.midX - toFrame.midX, y: 0.0), completion: { result in completeKey(.positionX, result) }) self.vertical.animatePositionAdditive(layer: layer, offset: CGPoint(x: 0.0, y: fromFrame.midY - toFrame.midY), completion: { result in completeKey(.positionY, result) }) self.horizontal.animateWidthAdditive(layer: layer, value: fromFrame.width - toFrame.width, completion: { result in completeKey(.sizeWidth, result) }) self.vertical.animateHeightAdditive(layer: layer, value: fromFrame.height - toFrame.height, completion: { result in completeKey(.sizeHeight, result) }) } public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) { let fromFrame = layer.frame layer.frame = frame self.animateFrame(layer: layer, from: fromFrame, completion: completion) } public func updateFrame(node: ASDisplayNode, frame: CGRect, completion: ((Bool) -> Void)? = nil) { let fromFrame = node.frame node.frame = frame self.animateFrame(layer: node.layer, from: fromFrame, completion: completion) } public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) { let fromPosition = layer.position layer.position = position enum Keys: CaseIterable { case positionX, positionY } var remainingKeys = Keys.allCases var completedValue = true let completeKey: (Keys, Bool) -> Void = { key, completed in remainingKeys.removeAll(where: { $0 == key }) if !completed { completedValue = false } if remainingKeys.isEmpty { completion?(completedValue) } } self.horizontal.animatePositionAdditive(layer: layer, offset: CGPoint(x: fromPosition.x - position.x, y: 0.0), completion: { result in completeKey(.positionX, result) }) self.vertical.animatePositionAdditive(layer: layer, offset: CGPoint(x: 0.0, y: fromPosition.y - position.y), completion: { result in completeKey(.positionY, result) }) } public func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { enum Keys: CaseIterable { case positionX, positionY } var remainingKeys = Keys.allCases var completedValue = true let completeKey: (Keys, Bool) -> Void = { key, completed in remainingKeys.removeAll(where: { $0 == key }) if !completed { completedValue = false } if remainingKeys.isEmpty { completion?(completedValue) } } self.horizontal.animatePositionAdditive(layer: layer, offset: CGPoint(x: offset.x, y: 0.0), to: CGPoint(x: toOffset.x, y: 0.0), completion: { result in completeKey(.positionX, result) }) self.vertical.animatePositionAdditive(layer: layer, offset: CGPoint(x: 0.0, y: offset.y), to: CGPoint(x: 0.0, y: toOffset.y), completion: { result in completeKey(.positionY, result) }) } } public extension ContainedViewLayoutTransition { func animateView(allowUserInteraction: Bool = false, delay: Double = 0.0, _ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: f() completion?(true) case let .animated(duration, curve): var options = curve.viewAnimationOptions if allowUserInteraction { options.insert(.allowUserInteraction) } UIView.animate(withDuration: duration, delay: delay, options: options, animations: { f() }, completion: completion) } } } public protocol ControlledTransitionAnimator: AnyObject { var duration: Double { get } func startAnimation() func setAnimationProgress(_ progress: CGFloat) func finishAnimation() func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?) func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) func updateContentsRect(layer: CALayer, contentsRect: CGRect, completion: ((Bool) -> Void)?) } protocol AnyValueProviding { var anyValue: ControlledTransitionProperty.AnyValue { get } } extension CGFloat: AnyValueProviding { func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat { let invT = 1.0 - fraction let result = other * fraction + self * invT return result } var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, nsValue: self as NSNumber, stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGFloat { return self == otherValue } else { return false } }, interpolate: { other, fraction in guard let otherValue = other.value as? CGFloat else { preconditionFailure() } return self.interpolate(with: otherValue, fraction: fraction).anyValue } ) } } extension Float: AnyValueProviding { func interpolate(with other: Float, fraction: CGFloat) -> Float { let invT = 1.0 - Float(fraction) let result = other * Float(fraction) + self * invT return result } var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, nsValue: self as NSNumber, stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? Float { return self == otherValue } else { return false } }, interpolate: { other, fraction in guard let otherValue = other.value as? Float else { preconditionFailure() } return self.interpolate(with: otherValue, fraction: fraction).anyValue } ) } } extension CGPoint: AnyValueProviding { func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint { return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction)) } var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, nsValue: NSValue(cgPoint: self), stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGPoint { return self == otherValue } else { return false } }, interpolate: { other, fraction in guard let otherValue = other.value as? CGPoint else { preconditionFailure() } return self.interpolate(with: otherValue, fraction: fraction).anyValue } ) } } extension CGSize: AnyValueProviding { func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize { return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction)) } var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, nsValue: NSValue(cgSize: self), stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGSize { return self == otherValue } else { return false } }, interpolate: { other, fraction in guard let otherValue = other.value as? CGSize else { preconditionFailure() } return self.interpolate(with: otherValue, fraction: fraction).anyValue } ) } } extension CGRect: AnyValueProviding { func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect { return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction)) } var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, nsValue: NSValue(cgRect: self), stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGRect { return self == otherValue } else { return false } }, interpolate: { other, fraction in guard let otherValue = other.value as? CGRect else { preconditionFailure() } return self.interpolate(with: otherValue, fraction: fraction).anyValue } ) } } final class ControlledTransitionProperty { final class AnyValue: Equatable, CustomStringConvertible { let value: Any let nsValue: Any let stringValue: () -> String let isEqual: (AnyValue) -> Bool let interpolate: (AnyValue, CGFloat) -> AnyValue init( value: Any, nsValue: Any, stringValue: @escaping () -> String, isEqual: @escaping (AnyValue) -> Bool, interpolate: @escaping (AnyValue, CGFloat) -> AnyValue ) { self.value = value self.nsValue = nsValue self.stringValue = stringValue self.isEqual = isEqual self.interpolate = interpolate } var description: String { return self.stringValue() } static func ==(lhs: AnyValue, rhs: AnyValue) -> Bool { if lhs.isEqual(rhs) { return true } else { return false } } } let layer: CALayer let path: String var fromValue: AnyValue let toValue: AnyValue private let completion: ((Bool) -> Void)? private lazy var animationKey: String = { return "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())" }() init(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding { self.layer = layer self.path = path self.fromValue = fromValue.anyValue self.toValue = toValue.anyValue self.completion = completion self.update(at: 0.0) } deinit { self.layer.removeAnimation(forKey: self.animationKey) } func update(at fraction: CGFloat) { let value = self.fromValue.interpolate(toValue, fraction) let animation = CABasicAnimation(keyPath: self.path) animation.speed = 0.0 animation.beginTime = CACurrentMediaTime() + 1000.0 animation.timeOffset = 0.01 animation.duration = 1.0 animation.fillMode = .both animation.fromValue = value.nsValue animation.toValue = value.nsValue animation.timingFunction = CAMediaTimingFunction(name: .linear) animation.isRemovedOnCompletion = false self.layer.add(animation, forKey: self.animationKey) } func complete(atEnd: Bool) { self.completion?(atEnd) } } public final class ControlledTransition { public final class NativeAnimator: ControlledTransitionAnimator { public let duration: Double private let curve: ContainedViewLayoutTransitionCurve private var animations: [ControlledTransitionProperty] = [] init( duration: Double, curve: ContainedViewLayoutTransitionCurve ) { self.duration = duration self.curve = curve } func merge(with other: NativeAnimator, forceRestart: Bool) { var removeAnimationIndices: [Int] = [] for i in 0 ..< self.animations.count { let animation = self.animations[i] var removeOtherAnimationIndices: [Int] = [] for j in 0 ..< other.animations.count { let otherAnimation = other.animations[j] if animation.layer === otherAnimation.layer && animation.path == otherAnimation.path { if animation.toValue == otherAnimation.toValue && !forceRestart { removeAnimationIndices.append(i) } else { removeOtherAnimationIndices.append(j) } } } for j in removeOtherAnimationIndices.reversed() { let otherAnimation = other.animations.remove(at: j) otherAnimation.complete(atEnd: false) } } for i in Set(removeAnimationIndices).sorted().reversed() { self.animations.remove(at: i).complete(atEnd: false) } } public func startAnimation() { } public func setAnimationProgress(_ progress: CGFloat) { let mappedFraction: CGFloat switch self.curve { case .spring: mappedFraction = springAnimationSolver(progress) case let .custom(c1x, c1y, c2x, c2y): mappedFraction = bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), progress) default: mappedFraction = progress } for animation in self.animations { animation.update(at: mappedFraction) } } public func finishAnimation() { for animation in self.animations { animation.update(at: 1.0) animation.complete(atEnd: true) } self.animations.removeAll() } private func add(animation: ControlledTransitionProperty) { for i in 0 ..< self.animations.count { let otherAnimation = self.animations[i] if otherAnimation.layer === animation.layer && otherAnimation.path == animation.path { let currentAnimation = self.animations[i] currentAnimation.complete(atEnd: false) self.animations.remove(at: i) break } } self.animations.append(animation) } public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) { if layer.opacity == Float(alpha) { return } let fromValue = layer.presentation()?.opacity ?? layer.opacity layer.opacity = Float(alpha) self.add(animation: ControlledTransitionProperty( layer: layer, path: "opacity", fromValue: fromValue, toValue: Float(alpha), completion: completion )) } public func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) { let t = layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale == scale { return } layer.transform = CATransform3DMakeScale(scale, scale, 1.0) self.add(animation: ControlledTransitionProperty( layer: layer, path: "transform.scale", fromValue: currentScale, toValue: scale, completion: completion )) } public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) { self.add(animation: ControlledTransitionProperty( layer: layer, path: "transform.scale", fromValue: fromValue, toValue: toValue, completion: completion )) } public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?) { self.add(animation: ControlledTransitionProperty( layer: layer, path: "position", fromValue: fromValue, toValue: toValue, completion: completion )) } public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) { if layer.position == position { return } let fromValue = layer.presentation()?.position ?? layer.position layer.position = position self.add(animation: ControlledTransitionProperty( layer: layer, path: "position", fromValue: fromValue, toValue: position, completion: completion )) } public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) { if layer.bounds == bounds { return } let fromValue: CGRect if let animationKeys = layer.animationKeys(), animationKeys.contains(where: { key in guard let animation = layer.animation(forKey: key) as? CAPropertyAnimation else { return false } if animation.keyPath == "bounds" { return true } else { return false } }) { fromValue = layer.presentation()?.bounds ?? layer.bounds } else { fromValue = layer.bounds } layer.bounds = bounds self.add(animation: ControlledTransitionProperty( layer: layer, path: "bounds", fromValue: fromValue, toValue: bounds, completion: completion )) } public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) { self.updatePosition(layer: layer, position: frame.center, completion: completion) self.updateBounds(layer: layer, bounds: CGRect(origin: CGPoint(), size: frame.size), completion: nil) } public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) { if layer.cornerRadius == cornerRadius { return } let fromValue = layer.presentation()?.cornerRadius ?? layer.cornerRadius layer.cornerRadius = cornerRadius self.add(animation: ControlledTransitionProperty( layer: layer, path: "cornerRadius", fromValue: fromValue, toValue: cornerRadius, completion: completion )) } public func updateContentsRect(layer: CALayer, contentsRect: CGRect, completion: ((Bool) -> Void)?) { if layer.contentsRect == contentsRect { return } let fromValue = layer.presentation()?.contentsRect ?? layer.contentsRect layer.contentsRect = contentsRect self.add(animation: ControlledTransitionProperty( layer: layer, path: "contentsRect", fromValue: fromValue, toValue: contentsRect, completion: completion )) } } public final class LegacyAnimator: ControlledTransitionAnimator { public let duration: Double public let transition: ContainedViewLayoutTransition init( duration: Double, curve: ContainedViewLayoutTransitionCurve ) { self.duration = duration if duration.isZero { self.transition = .immediate } else { self.transition = .animated(duration: duration, curve: curve) } } public func startAnimation() { } public func setAnimationProgress(_ progress: CGFloat) { } public func finishAnimation() { } public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) { self.transition.updateAlpha(layer: layer, alpha: alpha, completion: completion) } public func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) { self.transition.updateTransformScale(layer: layer, scale: scale, completion: completion) } public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) { self.transition.animateTransformScale(layer: layer, from: CGPoint(x: fromValue, y: fromValue), to: CGPoint(x: toValue, y: toValue), completion: completion) } public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) { self.transition.updatePosition(layer: layer, position: position, completion: completion) } public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?) { self.transition.animatePosition(layer: layer, from: fromValue, to: toValue, completion: completion) } public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) { self.transition.updateBounds(layer: layer, bounds: bounds, completion: completion) } public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) { self.transition.updateFrame(layer: layer, frame: frame, completion: completion) } public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) { self.transition.updateCornerRadius(layer: layer, cornerRadius: cornerRadius, completion: completion) } public func updateContentsRect(layer: CALayer, contentsRect: CGRect, completion: ((Bool) -> Void)?) { self.transition.updateContentsRect(layer: layer, contentsRect: contentsRect, completion: completion) } } public let animator: ControlledTransitionAnimator public let legacyAnimator: LegacyAnimator public init( duration: Double, curve: ContainedViewLayoutTransitionCurve, interactive: Bool ) { self.legacyAnimator = LegacyAnimator( duration: duration, curve: curve ) if interactive { self.animator = NativeAnimator( duration: duration, curve: curve ) } else { self.animator = self.legacyAnimator } } public func merge(with other: ControlledTransition, forceRestart: Bool) { if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator { animator.merge(with: otherAnimator, forceRestart: forceRestart) } } }