Repository: airbnb/lottie-ios
File: Sources/Private/CoreAnimation/CoreAnimationLayer.swift (Swift)

Code before:
  }

  /// Whether or not the animation is currently playing.
  ///  - Handles case where CAAnimations with a finite duration animation (e.g. `playOnce`)
  ///    have finished playing but still present on this layer.
  var isAnimationPlaying: Bool? {
    switch pendingAnimationConfiguration?.playbackState {
    case .playing:
      return true

    case .paused:
      return false

    case nil:
      switch playbackState {
      case .playing:
        guard let animationStartedAt, let currentAnimationConfiguration else { return false }
        if currentAnimationConfiguration.timingConfiguration.repeatCount == .greatestFiniteMagnitude {
          return true
        }

        let animationDuration = Double(
          currentAnimationConfiguration.animationContext.playTo -
            currentAnimationConfiguration.animationContext.playFrom) / animation.framerate
        let speed = Double(currentAnimationConfiguration.timingConfiguration.speed)
        let repeatCount = Double(currentAnimationConfiguration.timingConfiguration.repeatCount)
        let realDuration = animationDuration / speed * repeatCount
        return animationStartedAt.distance(to: Date()) < realDuration

      case nil, .paused:
        return false
      }
    }
  }

  /// The current frame of the animation being displayed,
  /// accounting for the realtime progress of any active CAAnimations.
  var currentFrame: AnimationFrameTime {
    get {
      switch playbackState {
      case .paused(let frame):
        return frame

      case .playing, nil:
        // When in the `playing` state, the animation is either actively playing
        // or is completed on the final frame of a non-repeating animation.
        // When a non-repeating animation is complete, `animation(forKey: #keyPath(animationProgress))`
        // is no longer present and the Core-Animation-managed `animationProgress` value is just 0.
        // In that case, since the animation is complete, we just return the final frame that was played to.
        let animationCurrentlyPlaying = animation(forKey: #keyPath(animationProgress)) != nil


Review comment (suggestion):
Please factor this logic into a method like `var expectedAnimationDuration: TimeInterval`. It would be great if you could include unit tests for the logic.

Code after:
}

// MARK: RootAnimationLayer

extension CoreAnimationLayer: RootAnimationLayer {

  var primaryAnimationKey: AnimationKey {
    .specific(#keyPath(animationProgress))
  }

  /// Whether or not the animation is currently playing.
  ///  - Handles case where CAAnimations with a finite duration animation (e.g. `playOnce`)
  ///    have finished playing but still present on this layer.
  var isAnimationPlaying: Bool? {
    switch pendingAnimationConfiguration?.playbackState {
    case .playing:
      return true

    case .paused:
      return false

    case nil:
      switch playbackState {
      case .playing:
        if animation(forKey: #keyPath(animationProgress)) != nil {
          return true
        }

        guard let animationStartedAt, let currentAnimationConfiguration else { return false }
        if currentAnimationConfiguration.timingConfiguration.repeatCount == .greatestFiniteMagnitude {
          return true
        } else {
          return animationStartedAt.distance(to: Date()) < currentAnimationConfiguration.expectedAnimationDuration
        }

      case nil, .paused:
        return false
      }
    }
  }

  /// The current frame of the animation being displayed,
  /// accounting for the realtime progress of any active CAAnimations.
  var currentFrame: AnimationFrameTime {
    get {
      switch playbackState {
      case .paused(let frame):
        return frame

      case .playing, nil:
        // When in the `playing` state, the animation is either actively playing