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

Code before:
      lottieAnimationLayer?.updateInFlightAnimation()
    } else {
      let currentFrame = currentFrame
      removeAnimations()
      self.currentFrame = currentFrame
    }
  }

}

// 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:
        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
      }
    }
  }


Review comment (suggestion):
This is clever, but changing it to no longer use the layer itself as the source of truth sounds like it will cause unexpected behavior changes. This is also the same approach used by the Main Thread rendering engine.

Code after:
    guard
      // Don't replace any pending animations that are queued to begin
      // on the next run loop cycle, since an existing pending animation
      // will cause the animation to be rebuilt anyway.
      pendingAnimationConfiguration == nil
    else { return }

    if isAnimationPlaying == true {
      lottieAnimationLayer?.updateInFlightAnimation()
    } else {
      let currentFrame = currentFrame
      removeAnimations()
      self.currentFrame = currentFrame
    }
  }

}

// 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
        }