Repository: airbnb/lottie-ios
File: Sources/Public/Animation/LottieAnimationView.swift (Swift)

Code before:
        let temp = animationContext.playFrom
        animationContext.playFrom = animationContext.playTo
        animationContext.playTo = temp
      }

      var timingConfiguration = CoreAnimationLayer.CAMediaTimingConfiguration(
        autoreverses: loopMode.caAnimationConfiguration.autoreverses,
        repeatCount: loopMode.caAnimationConfiguration.repeatCount,
        speed: abs(Float(animationSpeed)))

      // The animation should start playing from the `currentFrame`,
      // if `currentFrame` is included in the time range being played.
      let lowerBoundTime = min(animationContext.playFrom, animationContext.playTo)
      let upperBoundTime = max(animationContext.playFrom, animationContext.playTo)
      if (lowerBoundTime ..< upperBoundTime).contains(round(currentFrame)) {
        // We have to configure this differently depending on the loop mode:
        switch loopMode {
        // When playing exactly once (and not looping), we can just set the
        // `playFrom` time to be the `currentFrame`. Since the animation duration
        // is based on `playFrom` and `playTo`, this automatically truncates the
        // duration (so the animation stops playing at `playFrom`).
        //  - Don't do this is the animation is already at that frame
        //    (e.g. playing from 100% to 0% when the animation is already at 0%)
        //    since that would cause the animation to not play at all.
        case .playOnce:
          if animationContext.playTo != currentFrame {
            animationContext.playFrom = currentFrame
          }

        // When looping, we specifically _don't_ want to affect the duration of the animation,
        // since that would affect the duration of all subsequent loops. We just want to adjust
        // the duration of the _first_ loop. Instead of setting `playFrom`, we just add a `timeOffset`
        // so the first loop begins at `currentTime` but all subsequent loops are the standard duration.
        default:
          if animationSpeed < 0 {
            timingConfiguration.timeOffset = animation.time(forFrame: animationContext.playFrom) - currentTime
          } else {
            timingConfiguration.timeOffset = currentTime - animation.time(forFrame: animationContext.playFrom)
          }
        }
      }

      // If attempting to play a zero-duration animation, just pause on that single frame instead
      if animationContext.playFrom == animationContext.playTo {
        currentFrame = animationContext.playTo
        animationContext.closure.completionBlock?(true)
        return
      }

      coreAnimationLayer.playAnimation(configuration: .init(
        animationContext: animationContext,

Review comment (suggestion):
This is basically the same logic that the Main Thread rendering engine has here: https://github.com/airbnb/lottie-ios/blob/master/Sources/Public/Animation/LottieAnimationView.swift#L1422

Code after:
        let temp = animationContext.playFrom
        animationContext.playFrom = animationContext.playTo
        animationContext.playTo = temp
      }

      var timingConfiguration = CoreAnimationLayer.CAMediaTimingConfiguration(
        autoreverses: loopMode.caAnimationConfiguration.autoreverses,
        repeatCount: loopMode.caAnimationConfiguration.repeatCount,
        speed: abs(Float(animationSpeed)))

      // The animation should start playing from the `currentFrame`,
      // if `currentFrame` is included in the time range being played.
      let lowerBoundTime = min(animationContext.playFrom, animationContext.playTo)
      let upperBoundTime = max(animationContext.playFrom, animationContext.playTo)
      if (lowerBoundTime ..< upperBoundTime).contains(round(currentFrame)) {
        // We have to configure this differently depending on the loop mode:
        switch loopMode {
        // When playing exactly once (and not looping), we can just set the
        // `playFrom` time to be the `currentFrame`. Since the animation duration
        // is based on `playFrom` and `playTo`, this automatically truncates the
        // duration (so the animation stops playing at `playFrom`).
        //  - Don't do this if the animation is already at that frame
        //    (e.g. playing from 100% to 0% when the animation is already at 0%)
        //    since that would cause the animation to not play at all.
        case .playOnce:
          if animationContext.playTo != currentFrame {
            animationContext.playFrom = currentFrame
          }

        // When looping, we specifically _don't_ want to affect the duration of the animation,
        // since that would affect the duration of all subsequent loops. We just want to adjust
        // the duration of the _first_ loop. Instead of setting `playFrom`, we just add a `timeOffset`
        // so the first loop begins at `currentTime` but all subsequent loops are the standard duration.
        default:
          if animationSpeed < 0 {
            timingConfiguration.timeOffset = animation.time(forFrame: animationContext.playFrom) - currentTime
          } else {
            timingConfiguration.timeOffset = currentTime - animation.time(forFrame: animationContext.playFrom)
          }
        }
      }

      // If attempting to play a zero-duration animation, just pause on that single frame instead
      if animationContext.playFrom == animationContext.playTo {
        currentFrame = animationContext.playTo
        animationContext.closure.completionBlock?(true)
        return
      }

      coreAnimationLayer.playAnimation(configuration: .init(
        animationContext: animationContext,