// // KeyframeGroup.swift // lottie-swift // // Created by Brandon Withrow on 1/14/19. // import Foundation // MARK: - KeyframeGroup /// Used for coding/decoding a group of Keyframes by type. /// /// Keyframe data is wrapped in a dictionary { "k" : KeyframeData }. /// The keyframe data can either be an array of keyframes or, if no animation is present, the raw value. /// This helper object is needed to properly decode the json. final class KeyframeGroup { // MARK: Lifecycle init(keyframes: ContiguousArray>) { self.keyframes = keyframes } init(_ value: T) { keyframes = [Keyframe(value)] } // MARK: Internal enum KeyframeWrapperKey: String, CodingKey { case keyframeData = "k" } let keyframes: ContiguousArray> } // MARK: Decodable extension KeyframeGroup: Decodable where T: Decodable { convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: KeyframeWrapperKey.self) if let keyframeData: T = try? container.decode(T.self, forKey: .keyframeData) { /// Try to decode raw value; No keyframe data. self.init(keyframes: [Keyframe(keyframeData)]) } else { // Decode and array of keyframes. // // Body Movin and Lottie deal with keyframes in different ways. // // A keyframe object in Body movin defines a span of time with a START // and an END, from the current keyframe time to the next keyframe time. // // A keyframe object in Lottie defines a singular point in time/space. // This point has an in-tangent and an out-tangent. // // To properly decode this we must iterate through keyframes while holding // reference to the previous keyframe. var keyframesContainer = try container.nestedUnkeyedContainer(forKey: .keyframeData) var keyframes = ContiguousArray>() var previousKeyframeData: KeyframeData? while !keyframesContainer.isAtEnd { // Ensure that Time and Value are present. let keyframeData = try keyframesContainer.decode(KeyframeData.self) guard let value: T = keyframeData.startValue ?? previousKeyframeData?.endValue, let time = keyframeData.time else { /// Missing keyframe data. JSON must be corrupt. throw DecodingError.dataCorruptedError( forKey: KeyframeWrapperKey.keyframeData, in: container, debugDescription: "Missing keyframe data.") } keyframes.append(Keyframe( value: value, time: AnimationFrameTime(time), isHold: keyframeData.isHold, inTangent: previousKeyframeData?.inTangent, outTangent: keyframeData.outTangent, spatialInTangent: previousKeyframeData?.spatialInTangent, spatialOutTangent: keyframeData.spatialOutTangent)) previousKeyframeData = keyframeData } self.init(keyframes: keyframes) } } } // MARK: Encodable extension KeyframeGroup: Encodable where T: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: KeyframeWrapperKey.self) if keyframes.count == 1 { let keyframe = keyframes[0] try container.encode(keyframe.value, forKey: .keyframeData) } else { var keyframeContainer = container.nestedUnkeyedContainer(forKey: .keyframeData) for i in 1..( startValue: keyframe.value, endValue: nextKeyframe.value, time: keyframe.time, hold: keyframe.isHold ? 1 : nil, inTangent: nextKeyframe.inTangent, outTangent: keyframe.outTangent, spatialInTangent: nil, spatialOutTangent: nil) try keyframeContainer.encode(keyframeData) } } } } // MARK: DictionaryInitializable extension KeyframeGroup: DictionaryInitializable where T: AnyInitializable { convenience init(dictionary: [String: Any]) throws { var keyframes = ContiguousArray>() if let rawValue = dictionary[KeyframeWrapperKey.keyframeData.rawValue], let value = try? T(value: rawValue) { keyframes = [Keyframe(value)] } else { var frameDictionaries: [[String: Any]] if let singleFrameDictionary = dictionary[KeyframeWrapperKey.keyframeData.rawValue] as? [String: Any] { frameDictionaries = [singleFrameDictionary] } else { frameDictionaries = try dictionary.value(for: KeyframeWrapperKey.keyframeData) } var previousKeyframeData: KeyframeData? for frameDictionary in frameDictionaries { let data = try KeyframeData(dictionary: frameDictionary) guard let value: T = data.startValue ?? previousKeyframeData?.endValue, let time = data.time else { throw InitializableError.invalidInput } keyframes.append(Keyframe( value: value, time: time, isHold: data.isHold, inTangent: previousKeyframeData?.inTangent, outTangent: data.outTangent, spatialInTangent: previousKeyframeData?.spatialInTangent, spatialOutTangent: data.spatialOutTangent)) previousKeyframeData = data } } self.init(keyframes: keyframes) } } // MARK: Equatable extension KeyframeGroup: Equatable where T: Equatable { static func == (_ lhs: KeyframeGroup, _ rhs: KeyframeGroup) -> Bool { lhs.keyframes == rhs.keyframes } } // MARK: Hashable extension KeyframeGroup: Hashable where T: Hashable { func hash(into hasher: inout Hasher) { hasher.combine(keyframes) } } extension Keyframe { /// Creates a copy of this `Keyframe` with the same timing data, but a different value func withValue(_ newValue: Value) -> Keyframe { Keyframe( value: newValue, time: time, isHold: isHold, inTangent: inTangent, outTangent: outTangent, spatialInTangent: spatialInTangent, spatialOutTangent: spatialOutTangent) } } extension KeyframeGroup { /// Maps the values of each individual keyframe in this group func map(_ transformation: (T) throws -> NewValue) rethrows -> KeyframeGroup { KeyframeGroup(keyframes: ContiguousArray(try keyframes.map { keyframe in keyframe.withValue(try transformation(keyframe.value)) })) } } // MARK: - AnyKeyframeGroup /// A type-erased wrapper for `KeyframeGroup`s protocol AnyKeyframeGroup { var untyped: KeyframeGroup { get } } // MARK: - KeyframeGroup + AnyKeyframeGroup extension KeyframeGroup: AnyKeyframeGroup { /// An untyped copy of these keyframes var untyped: KeyframeGroup { map { $0 as Any } } }