Mantenga presionada la animation sin NSTimer

Actualización: el enfoque NSTimer funciona ahora, pero viene con un gran éxito de performance. La pregunta ahora se networkinguce a un enfoque sin NSTimers.

Estoy tratando de animar una animation interactiva "Presionar y mantener presionado". Después de seguir una carga de respuestas de SO, he seguido principalmente el enfoque en Controlling Animation Timing de @ david-rönnqvist. Y funciona, si utilizo un layer.timeOffset deslizante para pasar la layer.timeOffset .

Sin embargo, no puedo encontrar una buena manera de actualizar continuamente la misma animation en un gesto de presión y retención. La animation no se inicia, solo muestra el cuadro inicial o en algunos puntos termina y se niega a comenzar de nuevo.

¿Alguien puede ayudar a lograr el siguiente efecto, sin el horrible enfoque de NSTimer con el que estoy experimentando actualmente?

  1. Al presionar el usuario, comienza la animation, el círculo se llena.
  2. Mientras el usuario retiene (no necesariamente mueve el dedo), la animation debe continuar hasta el final y permanecer en ese marco.
  3. Cuando el usuario levanta el dedo, la animation debe retroceder, por lo que el círculo se vacía de nuevo.
  4. Si el usuario levanta el dedo durante la animation o presiona hacia abajo durante el reverso, la animation debe responder en consecuencia y llenar o vaciar desde el cuadro actual.

La animación que quiero lograr, pero presionando y manteniendo presionada

Aquí hay un repository de Github con mis esfuerzos actuales.

Como se mencionó, el siguiente código funciona bien. Está desencadenada por un control deslizante y hace su trabajo excelente.

 func animationTimeOffsetToPercentage(percentage: Double) { if fillShapeLayer == nil { fillShapeLayer = constructFillShapeLayer() } guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else { print("Animation not found") return } let timeOffset = maximumDuration * percentage print("Set animation to percentage \(percentage) with timeOffset: \(timeOffset)") fillAnimationLayer.timeOffset = timeOffset } 

Sin embargo, el siguiente enfoque con NSTimers funciona, pero tiene un golpe de performance increíble. Estoy buscando un enfoque que no use el NSTimer.

 func beginAnimation() { if fillShapeLayer == nil { fillShapeLayer = constructFillShapeLayer() } animationTimer?.invalidate() animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in if self.layer.timeOffset >= 1.0 { self.layer.timeOffset = self.maximumDuration } else { self.layer.timeOffset += 0.1 } }) } func reverseAnimation() { guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else { print("Animation not found") return } animationTimer?.invalidate() animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in if self.layer.timeOffset <= 0.0 { self.layer.timeOffset = 0.0 } else { self.layer.timeOffset -= 0.1 } }) } 

Cuando utiliza el slider, usa la capa fillAnimationLayer para la animation

 fillAnimationLayer.timeOffset = timeOffset 

Sin embargo, en las funciones beginAnimation y reverseAnimation estás utilizando self.layer .

Intente replace self.layer.timeOffset con self.fillShapeLayer!.timeOffset en sus bloques de timer.

La solución es doble;

  1. Asegúrese de que la animation no se elimine al finalizar y mantiene su marco final. Cumplido fácilmente con las siguientes líneas de código;

     animation.fillMode = kCAFillModeForwards animation.removedOnCompletion = false 
  2. La parte difícil; debe eliminar la animation original y comenzar una nueva animation inversa nueva que comienza en el punto correcto. Haciendo esto, me da el siguiente código;

     func setAnimation(layer: CAShapeLayer, startPath: AnyObject, endPath: AnyObject, duration: Double) { // Always create a new animation. let animation: CABasicAnimation = CABasicAnimation(keyPath: "path") if let currentAnimation = layer.animationForKey("animation") as? CABasicAnimation { // If an animation exists, reverse it. animation.fromValue = currentAnimation.toValue animation.toValue = currentAnimation.fromValue let pauseTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) // For the timeSinceStart, we take the minimum from the duration or the time passed. // If not, holding the animation longer than its duration would cause a delay in the reverse animation. let timeSinceStart = min(pauseTime - startTime, currentAnimation.duration) // Now convert for the reverse animation. let reversePauseTime = currentAnimation.duration - timeSinceStart animation.beginTime = pauseTime - reversePauseTime // Remove the old animation layer.removeAnimationForKey("animation") // Reset startTime, to be when the reverse WOULD HAVE started. startTime = animation.beginTime } else { // This happens when there is no current animation happening. startTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) animation.fromValue = startPath animation.toValue = endPath } animation.duration = duration animation.fillMode = kCAFillModeForwards animation.removedOnCompletion = false layer.addAnimation(animation, forKey: "animation") } 

Este artículo de Apple explica cómo hacer una animation adecuada de pausa y reanudación, que se convierte para usar con la animation inversa.