Repository: airbnb/lottie-ios
File: Sources/Private/Experimental/Extensions/KeyframeGroup+CAKeyframeAnimation.swift (Swift)

Code before:
    // the representation used by `CAKeyframeAnimation`
    var values = [Any]()
    var keyTimes = [NSNumber]()

    for keyframeModel in keyframes {
      let keyframe = CAKeyframe(
        from: keyframeModel,
        using: keyframeValueMapping,
        context: context)

      values.append(keyframe.value)
      keyTimes.append(NSNumber(value: Float(keyframe.keyTime)))
    }

    // Validate that we have correct start (0.0) and end (1.0) keyframes.
    //
    // From the documentation of `CAKeyframeAnimation.keyTimes`:
    //
    //   If the `calculationMode` is set to `linear` or `cubic`, the first value in the array
    //   must be 0.0 and the last value must be 1.0. All intermediate values represent
    //   time points between the start and end times.
    //
    if keyTimes.first != 0.0 {
      keyTimes.insert(0.0, at: 0)
      values.insert(values.first!, at: 0)
    }

    if keyTimes.last != 1.0 {
      keyTimes.append(1.0)
      values.append(values.last!)
    }

    animation.values = values
    animation.keyTimes = keyTimes

    return animation
  }

}

// MARK: - CAKeyframe

/// All of the information that describes a single keyframe in a `CAKeyframeAnimation`
fileprivate struct CAKeyframe<ValueRepresentation> {
  /// The value that will be applied to the keyPath of the layer being animated
  let value: ValueRepresentation

  /// The relative time (between 0 and 1) when this keyframe should be applied
  let keyTime: CGFloat

  // TODO: Support other properties like `timingFunctions`

Review comment (suggestion):
This was quite the tricky issue I ran in to -- the documentation for `keyTimes` says "the first value in the array must be 0.0 and the last value must be 1.0". 

This doesn't seems _strictly required_ at runtime (because it worked correctly at runtime in the previous PR) **BUT** the `layer.presentation()` copy seems to silently ignore any keyframes that don't follow this rule.

Code after:
    for keyframeModel in keyframes {
      let keyframe = CAKeyframe(
        from: keyframeModel,
        using: keyframeValueMapping,
        context: context)

      values.append(keyframe.value)
      keyTimes.append(NSNumber(value: Float(keyframe.keyTime)))
    }

    // Validate that we have correct start (0.0) and end (1.0) keyframes.
    //
    // From the documentation of `CAKeyframeAnimation.keyTimes`:
    //
    //   If the `calculationMode` is set to `linear` or `cubic`, the first value in the array
    //   must be 0.0 and the last value must be 1.0. All intermediate values represent
    //   time points between the start and end times.
    //
    if keyTimes.first != 0.0 {
      keyTimes.insert(0.0, at: 0)
      values.insert(values.first!, at: 0)
    }

    if keyTimes.last != 1.0 {
      keyTimes.append(1.0)
      values.append(values.last!)
    }

    animation.values = values
    animation.keyTimes = keyTimes

    return animation
  }

}

// MARK: - CAKeyframe

/// All of the information that describes a single keyframe in a `CAKeyframeAnimation`
fileprivate struct CAKeyframe<ValueRepresentation> {
  /// The value that will be applied to the keyPath of the layer being animated
  let value: ValueRepresentation

  /// The relative time (between 0 and 1) when this keyframe should be applied
  let keyTime: AnimationProgressTime

  // TODO: Support other properties like `timingFunctions`
}

extension CAKeyframe {
  /// Converts the given `Keyframe` data model into a `CAKeyframe` representation