private class TransitionAnimator { let groupName = "transition.group" weak var layer: CALayer? init(_ view: UIView) { self.layer = view.layer } private var group: CAAnimationGroup? { return layer?.animation(forKey: groupName) as? CAAnimationGroup } private func addGroup(_ animations: [CAAnimation], duration: TimeInterval) { guard let layer = layer else { return } layer.speed = 1 layer.timeOffset = 0 let t = CACurrentMediaTime() layer.beginTime = t layer.setValue(t, forKey: "lastStartedAt") layer.setValue(0, forKey: "currentDuration") layer.removeAllAnimations() let g = CAAnimationGroup() g.animations = animations g.duration = duration layer.add(g, forKey: groupName) } func start(_ animations: [CAAnimation], duration: TimeInterval) { addGroup(animations, duration: duration) pause() } func seek(_ progress: CGFloat) { guard let layer = layer, let duration = group?.duration else { return } pause() let s = CACurrentMediaTime() layer.setValue(s, forKey: "lastStartedAt") layer.beginTime = s let currentDuration = duration * 2 * CFTimeInterval(progress) layer.setValue(currentDuration, forKey: "currentDuration") layer.timeOffset = currentDuration } func pause() { guard let layer = layer else { return } let t = CACurrentMediaTime() layer.beginTime = t let currentDuration = (layer.value(forKey: "currentDuration") as! TimeInterval) + Double(layer.speed) * (t - (layer.value(forKey: "lastStartedAt") as! TimeInterval)) layer.setValue(currentDuration, forKey: "currentDuration") layer.speed = 0 layer.timeOffset = currentDuration } func resume() { guard let layer = layer else { return } layer.speed = 1 let s = CACurrentMediaTime() layer.setValue(s, forKey: "lastStartedAt") layer.beginTime = s } func cancel(isAnimated: Bool = true) { guard let layer = layer, let group = self.group else { return } guard isAnimated else { layer.removeAnimation(forKey: groupName) return } let newDuration = layer.timeOffset let newAnimations: [CABasicAnimation] = group.animations!.compactMap { $0 as? CABasicAnimation }.map { let a = CABasicAnimation() a.keyPath = $0.keyPath a.toValue = layer.value(forKeyPath: $0.keyPath!) a.fromValue = layer.presentation()!.value(forKeyPath: $0.keyPath!) return a } layer.removeAnimation(forKey: groupName) addGroup(newAnimations, duration: newDuration) } func finish() { guard let layer = layer, let group = self.group else { return } group.animations!.compactMap { $0 as? CABasicAnimation }.forEach { layer.setValue($0.toValue, forKeyPath: $0.keyPath!) } resume() } }