提交 1feddf38 编写于 作者: lizhongyi_'s avatar lizhongyi_

调整lottie-ios 使其支持iOS 12.0

上级 f0b90df0
......@@ -15,7 +15,7 @@
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>3B52.1</string>
</array>
</dict>
</array>
......
{
"deploymentTarget": "13.0",
"deploymentTarget": "12.0",
"validArchitectures": [
"arm64",
"x86_64"
......
......@@ -15,7 +15,7 @@
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>3B52.1</string>
</array>
</dict>
</array>
......
......@@ -305,9 +305,9 @@ extension CALayer {
// to segments based on the `CAAnimationCalculationMode`, so we can just
// check the first keyframe.
if keyframes[0].isHold {
.discrete
return .discrete
} else {
.linear
return .linear
}
}
......
......@@ -280,12 +280,12 @@ extension CALayer {
anchor, position, positionX, positionY, scale, rotationX, rotationY, rotationZ, skew, skewAxis
-> Hold<CATransform3D> in
let transformPosition: CGPoint =
if transformModel._positionX != nil, transformModel._positionY != nil {
CGPoint(x: positionX.cgFloatValue, y: positionY.cgFloatValue)
} else {
position.pointValue
}
let transformPosition: CGPoint
if transformModel._positionX != nil, transformModel._positionY != nil {
transformPosition = CGPoint(x: positionX.cgFloatValue, y: positionY.cgFloatValue)
} else {
transformPosition = position.pointValue
}
let transform = CATransform3D.makeTransform(
anchor: anchor.pointValue,
......
......@@ -308,8 +308,8 @@ final class CoreAnimationLayer: BaseAnimationLayer {
add(timedProgressAnimation, forKey: #keyPath(animationProgress))
}
/// Removes the current `CAAnimation`s, and rebuilds new animations
/// using the same configuration as the previous animations.
// Removes the current `CAAnimation`s, and rebuilds new animations
// using the same configuration as the previous animations.
private func rebuildCurrentAnimation() {
guard
// Don't replace any pending animations that are queued to begin
......@@ -343,15 +343,15 @@ extension CoreAnimationLayer: RootAnimationLayer {
var isAnimationPlaying: Bool? {
switch pendingAnimationConfiguration?.playbackState {
case .playing:
true
return true
case .paused:
false
return false
case nil:
switch playbackState {
case .playing:
animation(forKey: #keyPath(animationProgress)) != nil
return animation(forKey: #keyPath(animationProgress)) != nil
case nil, .paused:
false
return false
}
}
}
......@@ -537,26 +537,11 @@ extension CoreAnimationLayer: RootAnimationLayer {
/// every frame of every animation. For very large animations with a huge number of layers,
/// this can be prohibitively expensive.
func validateReasonableNumberOfTimeRemappingLayers() throws {
let numberOfLayersWithTimeRemapping = numberOfLayersWithTimeRemapping
let numberOfFrames = Int(animation.framerate * animation.duration)
let totalCost = numberOfLayersWithTimeRemapping * numberOfFrames
/// Cap the cost / complexity of animations that use Core Animation time remapping.
/// - Short, simple animations perform well, but long and complex animations perform poorly.
/// - We count the total number of frames that will need to be manually interpolated, which is
/// the number of layers with time remapping enabled times the total number of frames.
/// - The cap is arbitrary, and is currently:
/// - 1000 layers for a one second animation at 60fp
/// - 500 layers for a two second animation at 60fps, etc
/// - All of the sample animations in the lottie-ios repo below this cap perform well.
/// If users report animations below this cap that perform poorly, we can lower the cap.
let maximumAllowedCost = 1000 * 60
try layerContext.compatibilityAssert(
totalCost < maximumAllowedCost,
numberOfLayersWithTimeRemapping < 500,
"""
This animation has a very large number of layers with time remapping (\(numberOfLayersWithTimeRemapping) \
layers over \(numberOfFrames) frames) so will perform poorly with the Core Animation rendering engine.
This animation has a very large number of layers with time remapping (\(numberOfLayersWithTimeRemapping)),
so will perform poorly with the Core Animation rendering engine.
""")
}
......
......@@ -320,9 +320,9 @@ extension KeyframeGroup {
/// The value to use for a combined set of keyframes, for the given index
fileprivate func valueForCombinedKeyframes(at index: Int) -> T {
if keyframes.count == 1 {
keyframes[0].value
return keyframes[0].value
} else {
keyframes[index].value
return keyframes[index].value
}
}
}
......@@ -15,7 +15,7 @@ protocol AnimationLayer: CALayer {
// MARK: - LayerAnimationContext
/// Context describing the timing parameters of the current animation
// Context describing the timing parameters of the current animation
struct LayerAnimationContext {
/// The animation being played
let animation: LottieAnimation
......@@ -74,12 +74,12 @@ struct LayerAnimationContext {
// Normal animation playback (like when looping) skips the last frame.
// However when the animation is paused, we need to be able to render the final frame.
// To allow this we have to extend the length of the animation by one frame.
let animationEndFrame: AnimationFrameTime =
if timingConfiguration.speed == 0 {
animation.endFrame + 1
} else {
animation.endFrame
}
let animationEndFrame: AnimationFrameTime
if timingConfiguration.speed == 0 {
animationEndFrame = animation.endFrame + 1
} else {
animationEndFrame = animation.endFrame
}
return Double(animationEndFrame - animation.startFrame) / animation.framerate
}
......
......@@ -118,19 +118,19 @@ final class ShapeItemLayer: BaseAnimationLayer {
// We have to build a different layer hierarchy depending on if
// we're rendering a gradient (a `CAGradientLayer` masked by a `CAShapeLayer`)
// or a solid shape (a simple `CAShapeLayer`).
let fillLayerConfiguration: FillLayerConfiguration =
if let gradientFill = otherItems.first(GradientFill.self) {
setupGradientFillLayerHierarchy(for: gradientFill)
} else {
setupSolidFillLayerHierarchy()
}
let fillLayerConfiguration: FillLayerConfiguration
if let gradientFill = otherItems.first(GradientFill.self) {
fillLayerConfiguration = setupGradientFillLayerHierarchy(for: gradientFill)
} else {
fillLayerConfiguration = setupSolidFillLayerHierarchy()
}
let gradientStrokeConfiguration: GradientLayers? =
if let gradientStroke = otherItems.first(GradientStroke.self) {
setupGradientStrokeLayerHierarchy(for: gradientStroke)
} else {
nil
}
let gradientStrokeConfiguration: GradientLayers?
if let gradientStroke = otherItems.first(GradientStroke.self) {
gradientStrokeConfiguration = setupGradientStrokeLayerHierarchy(for: gradientStroke)
} else {
gradientStrokeConfiguration = nil
}
sublayerConfiguration = (fillLayerConfiguration, gradientStrokeConfiguration)
}
......
......@@ -308,11 +308,11 @@ extension ShapeItem {
var drawsCGPath: Bool {
switch type {
case .ellipse, .rectangle, .shape, .star:
true
return true
case .fill, .gradientFill, .group, .gradientStroke, .merge,
.repeater, .round, .stroke, .trim, .transform, .unknown:
false
return false
}
}
......@@ -320,11 +320,11 @@ extension ShapeItem {
var isFill: Bool {
switch type {
case .fill, .gradientFill:
true
return true
case .ellipse, .rectangle, .shape, .star, .group, .gradientStroke,
.merge, .repeater, .round, .stroke, .trim, .transform, .unknown:
false
return false
}
}
......@@ -332,18 +332,18 @@ extension ShapeItem {
var isStroke: Bool {
switch type {
case .stroke, .gradientStroke:
true
return true
case .ellipse, .rectangle, .shape, .star, .group, .gradientFill,
.merge, .repeater, .round, .fill, .trim, .transform, .unknown:
false
return false
}
}
/// For any inherited shape items that are affected by scaling (e.g. strokes but not fills),
/// any `ShapeTransform` in the given child group isn't supposed to be applied to the item.
/// To cancel out the effect of the transform, we can apply an inverse transform to the
/// shape item.
// For any inherited shape items that are affected by scaling (e.g. strokes but not fills),
// any `ShapeTransform` in the given child group isn't supposed to be applied to the item.
// To cancel out the effect of the transform, we can apply an inverse transform to the
// shape item.
func scaledCopyForChildGroup(_ childGroup: Group, context: LayerContext) throws -> ShapeItem {
guard
// Path-drawing items aren't inherited by child groups in this way
......@@ -502,15 +502,15 @@ extension [ShapeItemLayer.Item] {
// A `CAShapeLayer` can only draw a stroke on top of a fill -- if the fill is supposed to be
// drawn on top of the stroke, then they have to be rendered as separate layers.
let strokeDrawnOnTopOfFill: Bool =
if
let strokeIndex = strokesAndFills.firstIndex(where: { $0.item.isStroke }),
let fillIndex = strokesAndFills.firstIndex(where: { $0.item.isFill })
{
strokeIndex < fillIndex
} else {
false
}
let strokeDrawnOnTopOfFill: Bool
if
let strokeIndex = strokesAndFills.firstIndex(where: { $0.item.isStroke }),
let fillIndex = strokesAndFills.firstIndex(where: { $0.item.isFill })
{
strokeDrawnOnTopOfFill = strokeIndex < fillIndex
} else {
strokeDrawnOnTopOfFill = false
}
// `Fill` and `Stroke` items have an `alpha` property that can be animated separately,
// but each layer only has a single `opacity` property. We can only use a single `CAShapeLayer`
......
......@@ -40,8 +40,8 @@ final class ValueProviderStore {
valueProviders.append((keypath: keypath, valueProvider: valueProvider))
}
/// Retrieves the custom value keyframes for the given property,
/// if an `AnyValueProvider` was registered for the given keypath.
// Retrieves the custom value keyframes for the given property,
// if an `AnyValueProvider` was registered for the given keypath.
func customKeyframes<Value>(
of customizableProperty: CustomizableProperty<Value>,
for keypath: AnimationKeypath,
......@@ -115,9 +115,9 @@ extension AnyValueProviderStorage {
var isSupportedByCoreAnimationRenderingEngine: Bool {
switch self {
case .singleValue, .keyframes:
true
return true
case .closure:
false
return false
}
}
}
......
......@@ -23,8 +23,9 @@ When doing this, follow these steps:
3. Change all of the `public` symbols defined in this module to instead be `internal`
to prevent Lottie from exposing any EpoxyCore APIs.
4. Namespace any types that conflict with other existing types.
4. Add `@available` annotations as necessary so the library compiles
(Lottie has a lower minimum iOS version than EpoxyCore).
5. Namespace any types that conflict with other existing types.
For example, the EpoxyCore `Entry` type conflicts with the ZipFoundation `Entry` type,
so the EpoxyCore type has been renamed to `EpoxyEntry`.
5. Delete `EpoxySwiftUIHostingController.swift` and `EpoxySwiftUIHostingView.swift`, which are not used by Lottie, and emit deprecation warnings when building for visionOS.
// Created by eric_horacek on 10/8/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
#if canImport(SwiftUI) && !os(macOS)
import SwiftUI
// MARK: - EpoxySwiftUIUIHostingController
/// A `UIHostingController` that hosts SwiftUI views within an Epoxy container, e.g. an Epoxy
/// `CollectionView`.
///
/// Exposed internally to allow consumers to reason about these view controllers, e.g. to opt
/// collection view cells out of automated view controller impression tracking.
///
/// - SeeAlso: `EpoxySwiftUIHostingView`
@available(iOS 13.0, tvOS 13.0, *)
open class EpoxySwiftUIHostingController<Content: View>: UIHostingController<Content> {
// MARK: Lifecycle
/// Creates a `UIHostingController` that optionally ignores the `safeAreaInsets` when laying out
/// its contained `RootView`.
convenience init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
// We unfortunately need to call a private API to disable the safe area. We can also accomplish
// this by dynamically subclassing this view controller's view at runtime and overriding its
// `safeAreaInsets` property and returning `.zero`. An implementation of that logic is
// available in this file in the `2d28b3181cca50b89618b54836f7a9b6e36ea78e` commit if this API
// no longer functions in future SwiftUI versions.
_disableSafeArea = ignoreSafeArea
}
// MARK: Open
open override func viewDidLoad() {
super.viewDidLoad()
// A `UIHostingController` has a system background color by default as it's typically used in
// full-screen use cases. Since we're using this view controller to place SwiftUI views within
// other view controllers we default the background color to clear so we can see the views
// below, e.g. to draw highlight states in a `CollectionView`.
view.backgroundColor = .clear
}
}
#endif
// Created by eric_horacek on 9/16/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
#if canImport(Combine) && canImport(SwiftUI) && !os(macOS)
import Combine
import SwiftUI
// MARK: - SwiftUIHostingViewReuseBehavior
/// The reuse behavior of an `EpoxySwiftUIHostingView`.
enum SwiftUIHostingViewReuseBehavior: Hashable {
/// Instances of a `EpoxySwiftUIHostingView` with `RootView`s of same type can be reused within
/// the Epoxy container.
///
/// This is the default reuse behavior.
case reusable
/// Instances of a `EpoxySwiftUIHostingView` with `RootView`s of same type can only reused within
/// the Epoxy container when they have identical `reuseID`s.
case unique(reuseID: AnyHashable)
}
// MARK: - CallbackContextEpoxyModeled
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension CallbackContextEpoxyModeled
where
Self: WillDisplayProviding & DidEndDisplayingProviding,
CallbackContext: ViewProviding & AnimatedProviding
{
/// Updates the appearance state of a `EpoxySwiftUIHostingView` in coordination with the
/// `willDisplay` and `didEndDisplaying` callbacks of this `EpoxyableModel`.
///
/// - Note: You should only need to call then from the implementation of a concrete
/// `EpoxyableModel` convenience vendor method, e.g. `SwiftUI.View.itemModel(…)`.
func linkDisplayLifecycle<RootView: View>() -> Self
where
CallbackContext.View == EpoxySwiftUIHostingView<RootView>
{
willDisplay { context in
context.view.handleWillDisplay(animated: context.animated)
}
.didEndDisplaying { context in
context.view.handleDidEndDisplaying(animated: context.animated)
}
}
}
// MARK: - EpoxySwiftUIHostingView
/// A `UIView` that hosts a SwiftUI view within an Epoxy container, e.g. an Epoxy `CollectionView`.
///
/// Wraps an `EpoxySwiftUIHostingController` and adds it as a child view controller to the next
/// ancestor view controller in the hierarchy.
///
/// There's a private API that accomplishes this same behavior without needing a `UIViewController`:
/// `_UIHostingView`, but we can't safely use it as 1) the behavior may change out from under us, 2)
/// the API is private and 3) the `_UIHostingView` doesn't not accept setting a new `View` instance.
///
/// - SeeAlso: `EpoxySwiftUIHostingController`
@available(iOS 13.0, tvOS 13.0, *)
final class EpoxySwiftUIHostingView<RootView: View>: UIView, EpoxyableView {
// MARK: Lifecycle
init(style: Style) {
// Ignore the safe area to ensure the view isn't laid out incorrectly when being sized while
// overlapping the safe area.
epoxyContent = EpoxyHostingContent(rootView: style.initialContent.rootView)
viewController = EpoxySwiftUIHostingController(
rootView: .init(content: epoxyContent, environment: epoxyEnvironment),
ignoreSafeArea: true)
dataID = style.initialContent.dataID ?? DefaultDataID.noneProvided as AnyHashable
super.init(frame: .zero)
epoxyEnvironment.intrinsicContentSizeInvalidator = .init(invalidate: { [weak self] in
self?.viewController.view.invalidateIntrinsicContentSize()
// Inform the enclosing collection view that the size has changed, if we're contained in one,
// allowing the cell to resize.
//
// On iOS 16+, we could call `invalidateIntrinsicContentSize()` on the enclosing collection
// view cell instead, but that currently causes visual artifacts with `MagazineLayout`. The
// better long term fix is likely to switch to `UIHostingConfiguration` on iOS 16+ anyways.
if let enclosingCollectionView = self?.superview?.superview?.superview as? UICollectionView {
enclosingCollectionView.collectionViewLayout.invalidateLayout()
}
})
layoutMargins = .zero
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
struct Style: Hashable {
init(reuseBehavior: SwiftUIHostingViewReuseBehavior, initialContent: Content) {
self.reuseBehavior = reuseBehavior
self.initialContent = initialContent
}
var reuseBehavior: SwiftUIHostingViewReuseBehavior
var initialContent: Content
static func == (lhs: Style, rhs: Style) -> Bool {
lhs.reuseBehavior == rhs.reuseBehavior
}
func hash(into hasher: inout Hasher) {
hasher.combine(reuseBehavior)
}
}
struct Content: Equatable {
init(rootView: RootView, dataID: AnyHashable?) {
self.rootView = rootView
self.dataID = dataID
}
var rootView: RootView
var dataID: AnyHashable?
static func == (_: Content, _: Content) -> Bool {
// The content should never be equal since we need the `rootView` to be updated on every
// content change.
false
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
// We'll only be able to discover a valid parent `viewController` once we're added to a window,
// so we do so here in addition to the `handleWillDisplay(…)` method.
if window != nil {
addViewControllerIfNeeded()
}
}
func setContent(_ content: Content, animated _: Bool) {
// This triggers a change in the observed `EpoxyHostingContent` object and allows the
// propagation of the SwiftUI transaction, instead of just replacing the `rootView`.
epoxyContent.rootView = content.rootView
dataID = content.dataID ?? DefaultDataID.noneProvided as AnyHashable
// The view controller must be added to the view controller hierarchy to measure its content.
if window != nil {
addViewControllerIfNeeded()
}
// As of iOS 15.2, `UIHostingController` now renders updated content asynchronously, and as such
// this view will get sized incorrectly with the previous content when reused unless we invoke
// this semi-private API. We couldn't find any other method to get the view to resize
// synchronously after updating `rootView`, but hopefully this will become a internal API soon so
// we can remove this call.
viewController._render(seconds: 0)
// This is required to ensure that views with new content are properly resized.
viewController.view.invalidateIntrinsicContentSize()
}
override func layoutMarginsDidChange() {
super.layoutMarginsDidChange()
let margins = layoutMargins
switch effectiveUserInterfaceLayoutDirection {
case .rightToLeft:
epoxyEnvironment.layoutMargins = .init(
top: margins.top,
leading: margins.right,
bottom: margins.bottom,
trailing: margins.left)
case .leftToRight:
fallthrough
@unknown default:
epoxyEnvironment.layoutMargins = .init(
top: margins.top,
leading: margins.left,
bottom: margins.bottom,
trailing: margins.right)
}
// Allow the layout margins update to fully propagate through to the SwiftUI View before
// invalidating the layout.
DispatchQueue.main.async {
self.viewController.view.invalidateIntrinsicContentSize()
}
}
func handleWillDisplay(animated: Bool) {
guard state != .appeared, window != nil else { return }
transition(to: .appearing(animated: animated))
transition(to: .appeared)
}
func handleDidEndDisplaying(animated: Bool) {
guard state != .disappeared else { return }
transition(to: .disappearing(animated: animated))
transition(to: .disappeared)
}
// MARK: Private
private let viewController: EpoxySwiftUIHostingController<EpoxyHostingWrapper<RootView>>
private let epoxyContent: EpoxyHostingContent<RootView>
private let epoxyEnvironment = EpoxyHostingEnvironment()
private var dataID: AnyHashable
private var state: AppearanceState = .disappeared
/// Updates the appearance state of the `viewController`.
private func transition(to state: AppearanceState) {
guard state != self.state else { return }
// See "Handling View-Related Notifications" section for the state machine diagram.
// https://developer.apple.com/documentation/uikit/uiviewcontroller
switch (to: state, from: self.state) {
case (to: .appearing(let animated), from: .disappeared):
viewController.beginAppearanceTransition(true, animated: animated)
addViewControllerIfNeeded()
case (to: .disappearing(let animated), from: .appeared):
viewController.beginAppearanceTransition(false, animated: animated)
case (to: .disappeared, from: .disappearing):
removeViewControllerIfNeeded()
case (to: .appeared, from: .appearing):
viewController.endAppearanceTransition()
case (to: .disappeared, from: .appeared):
viewController.beginAppearanceTransition(false, animated: true)
removeViewControllerIfNeeded()
case (to: .appeared, from: .disappearing(let animated)):
viewController.beginAppearanceTransition(true, animated: animated)
viewController.endAppearanceTransition()
case (to: .disappeared, from: .appearing(let animated)):
viewController.beginAppearanceTransition(false, animated: animated)
removeViewControllerIfNeeded()
case (to: .appeared, from: .disappeared):
viewController.beginAppearanceTransition(true, animated: false)
addViewControllerIfNeeded()
viewController.endAppearanceTransition()
case (to: .appearing(let animated), from: .appeared):
viewController.beginAppearanceTransition(false, animated: animated)
viewController.beginAppearanceTransition(true, animated: animated)
case (to: .appearing(let animated), from: .disappearing):
viewController.beginAppearanceTransition(true, animated: animated)
case (to: .disappearing(let animated), from: .disappeared):
viewController.beginAppearanceTransition(true, animated: animated)
addViewControllerIfNeeded()
viewController.beginAppearanceTransition(false, animated: animated)
case (to: .disappearing(let animated), from: .appearing):
viewController.beginAppearanceTransition(false, animated: animated)
case (to: .appearing, from: .appearing),
(to: .appeared, from: .appeared),
(to: .disappearing, from: .disappearing),
(to: .disappeared, from: .disappeared):
// This should never happen since we guard on identical states.
EpoxyLogger.shared.assertionFailure("Impossible state change from \(self.state) to \(state)")
}
self.state = state
}
private func addViewControllerIfNeeded() {
// This isn't great, and means that we're going to add this view controller as a child view
// controller of a view controller somewhere else in the hierarchy, which the author of that
// view controller may not be expecting. However there's not really a better pathway forward
// here without requiring a view controller instance to be passed all the way through, which is
// both burdensome and error-prone.
guard let nextViewController = superview?.next(UIViewController.self) else {
EpoxyLogger.shared.assertionFailure(
"""
Unable to add a UIHostingController view, could not locate a UIViewController in the \
responder chain for view with ID \(dataID) of type \(RootView.self).
""")
return
}
guard viewController.parent !== nextViewController else { return }
// If in a different parent, we need to first remove from it before we add.
if viewController.parent != nil {
removeViewControllerIfNeeded()
}
addViewController(to: nextViewController)
state = .appeared
}
private func addViewController(to parent: UIViewController) {
viewController.willMove(toParent: parent)
parent.addChild(viewController)
addSubview(viewController.view)
// Get the view controller's view to be sized correctly so that we don't have to wait for
// autolayout to perform a pass to do so.
viewController.view.frame = bounds
viewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
viewController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
viewController.view.topAnchor.constraint(equalTo: topAnchor),
viewController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
viewController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
])
viewController.didMove(toParent: parent)
}
private func removeViewControllerIfNeeded() {
guard viewController.parent != nil else { return }
viewController.willMove(toParent: nil)
viewController.view.removeFromSuperview()
viewController.removeFromParent()
viewController.didMove(toParent: nil)
}
}
// MARK: - AppearanceState
/// The appearance state of a `EpoxySwiftUIHostingController` contained within a
/// `EpoxySwiftUIHostingView`.
private enum AppearanceState: Equatable {
case appearing(animated: Bool)
case appeared
case disappearing(animated: Bool)
case disappeared
}
// MARK: - UIResponder
extension UIResponder {
/// Recursively traverses the responder chain upwards from this responder to its next responder
/// until the a responder of the given type is located, else returns `nil`.
@nonobjc
fileprivate func next<ResponderType>(_ type: ResponderType.Type) -> ResponderType? {
self as? ResponderType ?? next?.next(type)
}
}
// MARK: - EpoxyHostingContent
/// The object that is used to communicate changes in the root view to the
/// `EpoxySwiftUIHostingController`.
@available(iOS 13.0, tvOS 13.0, *)
final class EpoxyHostingContent<RootView: View>: ObservableObject {
// MARK: Lifecycle
init(rootView: RootView) {
_rootView = .init(wrappedValue: rootView)
}
// MARK: Internal
@Published var rootView: RootView
}
// MARK: - EpoxyHostingEnvironment
/// The object that is used to communicate values to SwiftUI views within an
/// `EpoxySwiftUIHostingController`, e.g. layout margins.
@available(iOS 13.0, tvOS 13.0, *)
final class EpoxyHostingEnvironment: ObservableObject {
@Published var layoutMargins = EdgeInsets()
@Published var intrinsicContentSizeInvalidator = EpoxyIntrinsicContentSizeInvalidator(invalidate: { })
}
// MARK: - EpoxyHostingWrapper
/// The wrapper view that is used to communicate values to SwiftUI views within an
/// `EpoxySwiftUIHostingController`, e.g. layout margins.
@available(iOS 13.0, tvOS 13.0, *)
struct EpoxyHostingWrapper<Content: View>: View {
@ObservedObject var content: EpoxyHostingContent<Content>
@ObservedObject var environment: EpoxyHostingEnvironment
var body: some View {
content.rootView
.environment(\.epoxyLayoutMargins, environment.layoutMargins)
.environment(\.epoxyIntrinsicContentSizeInvalidator, environment.intrinsicContentSizeInvalidator)
}
}
#endif
......@@ -29,6 +29,7 @@ struct EpoxyIntrinsicContentSizeInvalidator {
// MARK: - EnvironmentValues
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension EnvironmentValues {
/// A means of invalidating the intrinsic content size of the parent `EpoxySwiftUIHostingView`.
var epoxyIntrinsicContentSizeInvalidator: EpoxyIntrinsicContentSizeInvalidator {
......
......@@ -6,6 +6,7 @@ import SwiftUI
// MARK: - View
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension View {
/// Applies the layout margins from the parent `EpoxySwiftUIHostingView` to this `View`, if there
/// are any.
......@@ -21,6 +22,7 @@ extension View {
// MARK: - EnvironmentValues
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension EnvironmentValues {
/// The layout margins of the parent `EpoxySwiftUIHostingView`, else zero if there is none.
var epoxyLayoutMargins: EdgeInsets {
......@@ -31,6 +33,7 @@ extension EnvironmentValues {
// MARK: - EpoxyLayoutMarginsKey
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
private struct EpoxyLayoutMarginsKey: EnvironmentKey {
static let defaultValue = EdgeInsets()
}
......@@ -39,6 +42,7 @@ private struct EpoxyLayoutMarginsKey: EnvironmentKey {
/// A view modifier that applies the layout margins from an enclosing `EpoxySwiftUIHostingView` to
/// the modified `View`.
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
private struct EpoxyLayoutMarginsPadding: ViewModifier {
@Environment(\.epoxyLayoutMargins) var epoxyLayoutMargins
......
......@@ -6,6 +6,7 @@ import SwiftUI
// MARK: - StyledView
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurableView {
/// Returns a SwiftUI `View` representing this `EpoxyableView`.
///
......@@ -51,6 +52,7 @@ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurable
}
}
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension StyledView
where
Self: ContentConfigurableView & BehaviorsConfigurableView,
......@@ -94,6 +96,7 @@ extension StyledView
}
}
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension StyledView
where
Self: ContentConfigurableView & BehaviorsConfigurableView,
......@@ -135,6 +138,7 @@ extension StyledView
}
}
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension StyledView
where
Self: ContentConfigurableView & BehaviorsConfigurableView,
......
......@@ -13,6 +13,7 @@ import SwiftUI
/// `sizeThatFits(…)` method.
///
/// - SeeAlso: ``SwiftUIMeasurementContainer``
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
protocol MeasuringViewRepresentable: ViewRepresentableType
where
RepresentableViewType == SwiftUIMeasurementContainer<Content>
......@@ -31,6 +32,7 @@ protocol MeasuringViewRepresentable: ViewRepresentableType
// MARK: Extensions
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension MeasuringViewRepresentable {
/// Returns a copy of this view with its sizing strategy updated to the given `sizing` value.
func sizing(_ strategy: SwiftUIMeasurementContainerStrategy) -> Self {
......@@ -43,6 +45,7 @@ extension MeasuringViewRepresentable {
// MARK: Defaults
#if os(iOS) || os(tvOS)
@available(iOS 13.0, tvOS 13.0, *)
extension MeasuringViewRepresentable {
func _overrideSizeThatFits(
_ size: inout CGSize,
......@@ -84,6 +87,7 @@ extension MeasuringViewRepresentable {
}
#elseif os(macOS)
@available(macOS 10.15, *)
extension MeasuringViewRepresentable {
func _overrideSizeThatFits(
_ size: inout CGSize,
......
......@@ -13,6 +13,7 @@ import SwiftUI
/// height through the `SwiftUISizingContext` binding.
///
/// - SeeAlso: ``MeasuringViewRepresentable``
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
final class SwiftUIMeasurementContainer<Content: ViewType>: ViewType {
// MARK: Lifecycle
......@@ -24,12 +25,12 @@ final class SwiftUIMeasurementContainer<Content: ViewType>: ViewType {
// On iOS 15 and below, passing zero can result in a constraint failure the first time a view
// is displayed, but the system gracefully recovers afterwards. On iOS 16, it's fine to pass
// zero.
let initialSize: CGSize =
if #available(iOS 16, tvOS 16, macOS 13, *) {
.zero
} else {
.init(width: 375, height: 150)
}
let initialSize: CGSize
if #available(iOS 16, tvOS 16, macOS 13, *) {
initialSize = .zero
} else {
initialSize = .init(width: 375, height: 150)
}
super.init(frame: .init(origin: .zero, size: initialSize))
addSubview(content)
......@@ -180,16 +181,12 @@ final class SwiftUIMeasurementContainer<Content: ViewType>: ViewType {
resolved = .intrinsic(intrinsicSize)
}
}
case .proposed:
resolved = .proposed
case .intrinsicHeightProposedWidth:
resolved = .intrinsicHeightProposedWidth
case .intrinsicWidthProposedHeight:
resolved = .intrinsicWidthProposedHeight
case .intrinsic:
resolved = .intrinsic(content.systemLayoutFittingIntrinsicSize())
}
......@@ -227,15 +224,12 @@ final class SwiftUIMeasurementContainer<Content: ViewType>: ViewType {
case .proposed:
constraints[.trailing]?.priority = .required
constraints[.bottom]?.priority = .required
case .intrinsicHeightProposedWidth:
constraints[.trailing]?.priority = .required
constraints[.bottom]?.priority = .almostRequired
case .intrinsicWidthProposedHeight:
constraints[.trailing]?.priority = .almostRequired
constraints[.bottom]?.priority = .required
case .intrinsic:
constraints[.trailing]?.priority = .almostRequired
constraints[.bottom]?.priority = .almostRequired
......
......@@ -12,6 +12,7 @@ import SwiftUI
///
/// Includes an optional generic `Storage` value, which can be used to compare old and new values
/// across state changes to prevent redundant view updates.
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
struct SwiftUIView<Content: ViewType, Storage>: MeasuringViewRepresentable,
UIViewConfiguringSwiftUIView
{
......@@ -50,6 +51,7 @@ struct SwiftUIView<Content: ViewType, Storage>: MeasuringViewRepresentable,
// MARK: UIViewRepresentable
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension SwiftUIView {
func makeCoordinator() -> Coordinator {
Coordinator(storage: storage)
......@@ -96,6 +98,7 @@ extension SwiftUIView {
// MARK: SwiftUIView.ConfigurationContext
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension SwiftUIView {
/// The configuration context that's available to configure the `Content` view whenever the
/// `updateUIView()` method is invoked via a configuration closure.
......@@ -127,6 +130,7 @@ extension SwiftUIView {
// MARK: SwiftUIView.Coordinator
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension SwiftUIView {
/// A coordinator that stores the `storage` associated with this view, enabling the old storage
/// value to be accessed during the `updateUIView(…)`.
......
......@@ -6,6 +6,7 @@ import SwiftUI
// MARK: - ViewTypeProtocol + swiftUIView
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension ViewTypeProtocol {
/// Returns a SwiftUI `View` representing this `UIView`, constructed with the given `makeView`
/// closure and sized with the given sizing configuration.
......
......@@ -8,6 +8,7 @@ import SwiftUI
/// A protocol describing a SwiftUI `View` that can configure its `UIView` content via an array of
/// `configuration` closures.
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
protocol UIViewConfiguringSwiftUIView: View {
/// The context available to this configuration, which provides the `UIView` instance at a minimum
/// but can include additional context as needed.
......@@ -23,6 +24,7 @@ protocol UIViewConfiguringSwiftUIView: View {
// MARK: Extensions
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension UIViewConfiguringSwiftUIView {
/// Returns a copy of this view updated to have the given closure applied to its represented view
/// whenever it is updated via the `updateUIView(…)` method.
......
......@@ -12,12 +12,14 @@ typealias ViewType = UIView
/// The platform's SwiftUI view representable type.
/// Either `UIViewRepresentable` on iOS/tvOS or `NSViewRepresentable` on macOS.
@available(iOS 13.0, tvOS 13.0, *)
typealias ViewRepresentableType = UIViewRepresentable
/// The platform's layout constraint priority type.
/// Either `UILayoutPriority` on iOS/tvOS or `NSLayoutConstraint.Priority` on macOS.
typealias LayoutPriorityType = UILayoutPriority
@available(iOS 13.0, tvOS 13.0, *)
extension ViewRepresentableType {
/// The platform's view type for `ViewRepresentableType`.
/// Either `UIViewType` on iOS/tvOS or `NSViewType` on macOS.
......@@ -33,12 +35,14 @@ typealias ViewType = NSView
/// The platform's SwiftUI view representable type.
/// Either `UIViewRepresentable` on iOS/tvOS, or `NSViewRepresentable` on macOS.
@available(macOS 10.15, *)
typealias ViewRepresentableType = NSViewRepresentable
/// The platform's layout constraint priority type.
/// Either `UILayoutPriority` on iOS/tvOS, or `NSLayoutConstraint.Priority` on macOS.
typealias LayoutPriorityType = NSLayoutConstraint.Priority
@available(macOS 10.15, *)
extension ViewRepresentableType {
/// The platform's view type for `ViewRepresentableType`.
/// Either `UIViewType` on iOS/tvOS or `NSViewType` on macOS.
......
......@@ -216,7 +216,7 @@ extension LRUCache {
// MARK: Private
/// Remove container from list (must be called inside lock)
// Remove container from list (must be called inside lock)
private func remove(_ container: Container) {
if head === container {
head = container.next
......@@ -229,7 +229,7 @@ extension LRUCache {
container.next = nil
}
/// Append container to list (must be called inside lock)
// Append container to list (must be called inside lock)
private func append(_ container: Container) {
assert(container.next == nil)
if head == nil {
......@@ -240,7 +240,7 @@ extension LRUCache {
tail = container
}
/// Remove expired values (must be called outside lock)
// Remove expired values (must be called outside lock)
private func clean() {
lock.lock()
defer { lock.unlock() }
......
......@@ -62,7 +62,6 @@ extension Archive {
file: archiveFile,
endOfCentralDirectoryRecord: eocdRecord,
zip64EndOfCentralDirectory: zip64EOCD)
case .create:
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(
numberOfDisk: 0,
......@@ -77,7 +76,6 @@ extension Archive {
try endOfCentralDirectoryRecord.data.write(to: url, options: .withoutOverwriting)
} catch { return nil }
fallthrough
case .update:
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
guard
......@@ -98,12 +96,12 @@ extension Archive {
static func makeBackingConfiguration(for data: Data, mode: AccessMode)
-> BackingConfiguration?
{
let posixMode =
switch mode {
case .read: "rb"
case .create: "wb+"
case .update: "rb+"
}
let posixMode: String
switch mode {
case .read: posixMode = "rb"
case .create: posixMode = "wb+"
case .update: posixMode = "rb+"
}
let memoryFile = MemoryFile(data: data)
guard let archiveFile = memoryFile.open(mode: posixMode) else { return nil }
......@@ -118,7 +116,6 @@ extension Archive {
endOfCentralDirectoryRecord: eocdRecord,
zip64EndOfCentralDirectory: zip64EOCD,
memoryFile: memoryFile)
case .create:
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(
numberOfDisk: 0,
......@@ -133,7 +130,6 @@ extension Archive {
fwrite(buffer.baseAddress, buffer.count, 1, archiveFile) // Errors handled during read
}
fallthrough
case .update:
guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
return nil
......
......@@ -83,7 +83,6 @@ extension Archive {
bufferSize: bufferSize,
progress: progress,
provider: provider)
case .deflate:
(sizeWritten, checksum) = try writeCompressed(
size: uncompressedSize,
......@@ -91,11 +90,9 @@ extension Archive {
progress: progress,
provider: provider)
}
case .directory:
_ = try provider(0, 0)
if let progress { progress.completedUnitCount = progress.totalUnitCount }
case .symlink:
let (linkSizeWritten, linkChecksum) = try writeSymbolicLink(
size: Int(uncompressedSize),
......@@ -229,7 +226,6 @@ extension Archive {
throw ArchiveError.invalidCentralDirectoryEntryCount
}
return (sizeOfCD + UInt64(cdDataLengthChange), numberOfTotalEntries + UInt64(countChange))
case .remove:
return (sizeOfCD - UInt64(-cdDataLengthChange), numberOfTotalEntries - UInt64(-countChange))
}
......
......@@ -32,9 +32,9 @@ extension Archive {
func totalUnitCountForReading(_ entry: Entry) -> Int64 {
switch entry.type {
case .file, .symlink:
Int64(entry.uncompressedSize)
return Int64(entry.uncompressedSize)
case .directory:
defaultDirectoryUnitCount
return defaultDirectoryUnitCount
}
}
......
......@@ -52,7 +52,6 @@ extension Archive {
skipCRC32: skipCRC32,
progress: progress,
consumer: consumer)
case .directory:
let consumer = { (_: Data) in
try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
......@@ -63,7 +62,6 @@ extension Archive {
skipCRC32: skipCRC32,
progress: progress,
consumer: consumer)
case .symlink:
guard !fileManager.itemExists(at: url) else {
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
......@@ -123,7 +121,6 @@ extension Archive {
skipCRC32: skipCRC32,
progress: progress,
with: consumer)
case .deflate: checksum = try readCompressed(
entry: entry,
bufferSize: bufferSize,
......@@ -131,11 +128,9 @@ extension Archive {
progress: progress,
with: consumer)
}
case .directory:
try consumer(Data())
progress?.completedUnitCount = totalUnitCountForReading(entry)
case .symlink:
let localFileHeader = entry.localFileHeader
let size = Int(localFileHeader.compressedSize)
......
......@@ -96,7 +96,6 @@ extension Archive {
bufferSize: bufferSize,
progress: progress,
provider: provider)
case .directory:
provider = { _, _ in Data() }
try addEntry(
......@@ -109,7 +108,6 @@ extension Archive {
bufferSize: bufferSize,
progress: progress,
provider: provider)
case .symlink:
provider = { _, _ -> Data in
let linkDestination = try fileManager.destinationOfSymbolicLink(atPath: fileURL.path)
......
......@@ -169,7 +169,6 @@ extension Data {
if operation == COMPRESSION_STREAM_DECODE, !skipCRC32 { crc32 = outputData.crc32(checksum: crc32) }
stream.dst_ptr = destPointer
stream.dst_size = bufferSize
default: throw CompressionError.corruptedData
}
} while status == COMPRESSION_STATUS_OK
......
......@@ -121,9 +121,9 @@ extension Entry.ZIP64ExtendedInformation {
var size: Int {
switch self {
case .uncompressedSize, .compressedSize, .relativeOffsetOfLocalHeader:
8
return 8
case .diskNumberStart:
4
return 4
}
}
}
......
......@@ -185,7 +185,6 @@ struct Entry: Equatable {
default:
return isDirectory ? .directory : .file
}
case .msdos:
isDirectory = isDirectory || ((centralDirectoryStructure.externalFileAttributes >> 4) == 0x01)
fallthrough // For all other OSes we can only guess based on the directory suffix char
......
......@@ -44,22 +44,21 @@ extension FileManager {
let permissions = mode_t(externalFileAttributes >> 16) & ~S_IFMT
let defaultPermissions = entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
return permissions == 0 ? defaultPermissions : UInt16(permissions)
default:
return entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
}
}
class func externalFileAttributesForEntry(of type: Entry.EntryType, permissions: UInt16) -> UInt32 {
let typeInt =
switch type {
case .file:
UInt16(S_IFREG)
case .directory:
UInt16(S_IFDIR)
case .symlink:
UInt16(S_IFLNK)
}
var typeInt: UInt16
switch type {
case .file:
typeInt = UInt16(S_IFREG)
case .directory:
typeInt = UInt16(S_IFDIR)
case .symlink:
typeInt = UInt16(S_IFLNK)
}
var externalFileAttributes = UInt32(typeInt | UInt16(permissions))
externalFileAttributes = (externalFileAttributes << 16)
return externalFileAttributes
......
......@@ -36,10 +36,6 @@ class CompositionLayer: CALayer, KeypathSearchable {
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"sublayerTransform" : NSNull(),
"shadowOpacity" : NSNull(),
"shadowOffset" : NSNull(),
"shadowColor" : NSNull(),
"shadowRadius" : NSNull(),
]
contentsLayer.anchorPoint = .zero
......@@ -59,14 +55,6 @@ class CompositionLayer: CALayer, KeypathSearchable {
contentsLayer.mask = maskLayer
}
// There are two different drop shadow schemas, either using `DropShadowEffect` or `DropShadowStyle`.
// If both happen to be present, prefer the `DropShadowEffect` (which is the drop shadow schema
// supported on other platforms).
let dropShadowEffect = layer.effects.first(where: { $0 is DropShadowEffect }) as? DropShadowModel
let dropShadowStyle = layer.styles.first(where: { $0 is DropShadowStyle }) as? DropShadowModel
if let dropShadowModel = dropShadowEffect ?? dropShadowStyle {
layerEffectNodes.append(DropShadowNode(model: dropShadowModel))
}
name = layer.name
}
......@@ -84,7 +72,6 @@ class CompositionLayer: CALayer, KeypathSearchable {
keypathName = layer.keypathName
childKeypaths = [transformNode.transformProperties]
maskLayer = nil
layerEffectNodes = layer.layerEffectNodes
super.init(layer: layer)
}
......@@ -109,8 +96,6 @@ class CompositionLayer: CALayer, KeypathSearchable {
let startFrame: CGFloat
let timeStretch: CGFloat
var layerEffectNodes: [LayerEffectNode] = []
// MARK: Keypath Searchable
let keypathName: String
......@@ -157,10 +142,6 @@ class CompositionLayer: CALayer, KeypathSearchable {
contentsLayer.opacity = transformNode.opacity
contentsLayer.isHidden = !layerVisible
layerDelegate?.frameUpdated(frame: frame)
for layerEffectNode in layerEffectNodes {
layerEffectNode.updateWithFrame(layer: self, frame: frame)
}
}
func displayContentsWithFrame(frame _: CGFloat, forceUpdates _: Bool) {
......
......@@ -11,19 +11,19 @@ extension MaskMode {
var usableMode: MaskMode {
switch self {
case .add:
.add
return .add
case .subtract:
.subtract
return .subtract
case .intersect:
.intersect
return .intersect
case .lighten:
.add
return .add
case .darken:
.darken
return .darken
case .difference:
.intersect
return .intersect
case .none:
.none
return .none
}
}
}
......
......@@ -16,7 +16,6 @@ final class PreCompositionLayer: CompositionLayer {
asset: PrecompAsset,
layerImageProvider: LayerImageProvider,
layerTextProvider: LayerTextProvider,
layerFontProvider: LayerFontProvider,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
assetLibrary: AssetLibrary?,
......@@ -39,7 +38,6 @@ final class PreCompositionLayer: CompositionLayer {
assetLibrary: assetLibrary,
layerImageProvider: layerImageProvider,
layerTextProvider: layerTextProvider,
layerFontProvider: layerFontProvider,
textProvider: textProvider,
fontProvider: fontProvider,
frameRate: frameRate,
......@@ -79,7 +77,6 @@ final class PreCompositionLayer: CompositionLayer {
layerImageProvider.addImageLayers(imageLayers)
layerTextProvider.addTextLayers(textLayers)
layerFontProvider.addTextLayers(textLayers)
}
override init(layer: Any) {
......
......@@ -16,22 +16,22 @@ extension TextJustification {
var textAlignment: NSTextAlignment {
switch self {
case .left:
.left
return .left
case .right:
.right
return .right
case .center:
.center
return .center
}
}
var caTextAlignement: CATextLayerAlignmentMode {
switch self {
case .left:
.left
return .left
case .right:
.right
return .right
case .center:
.center
return .center
}
}
}
......@@ -121,24 +121,20 @@ final class TextCompositionLayer: CompositionLayer {
// Prior to Lottie 4.3.0 the Main Thread rendering engine always just used `LegacyAnimationTextProvider`
// and called it with the `keypathName` (only the last path component of the full keypath).
// Starting in Lottie 4.3.0 we use `AnimationKeypathTextProvider` instead if implemented.
let textString: String =
if let keypathTextValue = textProvider.text(for: fullAnimationKeypath, sourceText: text.text) {
keypathTextValue
} else if let legacyTextProvider = textProvider as? LegacyAnimationTextProvider {
legacyTextProvider.textFor(keypathName: keypathName, sourceText: text.text)
} else {
text.text
}
let textString: String
if let keypathTextValue = textProvider.text(for: fullAnimationKeypath, sourceText: text.text) {
textString = keypathTextValue
} else if let legacyTextProvider = textProvider as? LegacyAnimationTextProvider {
textString = legacyTextProvider.textFor(keypathName: keypathName, sourceText: text.text)
} else {
textString = text.text
}
let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
let start = rootNode?.textOutputNode.start.flatMap { Int($0) }
let end = rootNode?.textOutputNode.end.flatMap { Int($0) }
let selectedRangeOpacity = rootNode?.textOutputNode.selectedRangeOpacity
let textRangeUnit = rootNode?.textAnimatorProperties.textRangeUnit
// Set all of the text layer options
textLayer.text = textString
......@@ -147,12 +143,6 @@ final class TextCompositionLayer: CompositionLayer {
textLayer.lineHeight = CGFloat(text.lineHeight)
textLayer.tracking = tracking
// Configure the text animators
textLayer.start = start
textLayer.end = end
textLayer.textRangeUnit = textRangeUnit
textLayer.selectedRangeOpacity = selectedRangeOpacity
if let fillColor = rootNode?.textOutputNode.fillColor {
textLayer.fillColor = fillColor
} else if let fillColor = text.fillColorData?.cgColorValue {
......
......@@ -37,7 +37,6 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
assetLibrary: animation.assetLibrary,
layerImageProvider: layerImageProvider,
layerTextProvider: layerTextProvider,
layerFontProvider: layerFontProvider,
textProvider: textProvider,
fontProvider: fontProvider,
frameRate: CGFloat(animation.framerate),
......@@ -128,16 +127,16 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
public override func display() {
guard Thread.isMainThread else { return }
var newFrame: CGFloat =
if
let animationKeys = animationKeys(),
!animationKeys.isEmpty
{
presentation()?.currentFrame ?? currentFrame
} else {
// We ignore the presentation's frame if there's no animation in the layer.
currentFrame
}
var newFrame: CGFloat
if
let animationKeys = animationKeys(),
!animationKeys.isEmpty
{
newFrame = presentation()?.currentFrame ?? currentFrame
} else {
// We ignore the presentation's frame if there's no animation in the layer.
newFrame = currentFrame
}
if respectAnimationFrameRate {
newFrame = floor(newFrame)
}
......
......@@ -14,7 +14,6 @@ extension [LayerModel] {
assetLibrary: AssetLibrary?,
layerImageProvider: LayerImageProvider,
layerTextProvider: LayerTextProvider,
layerFontProvider: LayerFontProvider,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
frameRate: CGFloat,
......@@ -50,7 +49,6 @@ extension [LayerModel] {
asset: precompAsset,
layerImageProvider: layerImageProvider,
layerTextProvider: layerTextProvider,
layerFontProvider: layerFontProvider,
textProvider: textProvider,
fontProvider: fontProvider,
assetLibrary: assetLibrary,
......
......@@ -39,7 +39,7 @@ final class CoreTextRenderLayer: CALayer {
}
}
public var alignment = NSTextAlignment.left {
public var alignment: NSTextAlignment = .left {
didSet {
needsContentUpdate = true
setNeedsLayout()
......@@ -102,40 +102,6 @@ final class CoreTextRenderLayer: CALayer {
}
}
public var start: Int? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var end: Int? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
/// The type of unit to use when computing the `start` / `end` range within the text string
public var textRangeUnit: TextRangeUnit? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
/// The opacity to apply to the range between `start` and `end`
public var selectedRangeOpacity: CGFloat? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public func sizeToFit() {
updateTextContent()
bounds = drawingRect
......@@ -171,19 +137,19 @@ final class CoreTextRenderLayer: CALayer {
let drawingPath = CGPath(rect: drawingRect, transform: nil)
let fillFrame: CTFrame? =
if let setter = fillFrameSetter {
CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
nil
}
let fillFrame: CTFrame?
if let setter = fillFrameSetter {
fillFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
fillFrame = nil
}
let strokeFrame: CTFrame? =
if let setter = strokeFrameSetter {
CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
nil
}
let strokeFrame: CTFrame?
if let setter = strokeFrameSetter {
strokeFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
strokeFrame = nil
}
// This fixes a vertical padding issue that arises when drawing some fonts.
// For some reason some fonts, such as Helvetica draw with and ascender that is greater than the one reported by CTFontGetAscender.
......@@ -210,14 +176,15 @@ final class CoreTextRenderLayer: CALayer {
// MARK: Private
private var drawingRect = CGRect.zero
private var drawingAnchor = CGPoint.zero
private var drawingRect: CGRect = .zero
private var drawingAnchor: CGPoint = .zero
private var fillFrameSetter: CTFramesetter?
private var attributedString: NSAttributedString?
private var strokeFrameSetter: CTFramesetter?
private var needsContentUpdate = false
/// Draws Debug colors for the font alignment.
// Draws Debug colors for the font alignment.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
private func drawDebug(_ ctx: CGContext) {
if let font {
let ascent = CTFontGetAscent(font)
......@@ -293,58 +260,7 @@ final class CoreTextRenderLayer: CALayer {
attributes[NSAttributedString.Key.foregroundColor] = fillColor
}
let attrString = NSMutableAttributedString(string: text, attributes: attributes)
// Apply the text animator within between the `start` and `end` indices
if let selectedRangeOpacity {
// The start and end of a text animator refer to the portions of the text
// where that animator is applies. In the schema these can be represented
// in absolute index value, or as percentages relative to the dynamic string length.
var startIndex: Int
var endIndex: Int
switch textRangeUnit ?? .percentage {
case .index:
startIndex = start ?? 0
endIndex = end ?? text.count
case .percentage:
let startPercentage = Double(start ?? 0) / 100
let endPercentage = Double(end ?? 100) / 100
startIndex = Int(round(Double(attrString.length) * startPercentage))
endIndex = Int(round(Double(attrString.length) * endPercentage))
}
// Carefully cap the indices, since passing invalid indices
// to `NSAttributedString` will crash the app.
startIndex = startIndex.clamp(0, attrString.length)
endIndex = endIndex.clamp(0, attrString.length)
// Make sure the end index actually comes after the start index
if endIndex < startIndex {
swap(&startIndex, &endIndex)
}
// Apply the `selectedRangeOpacity` to the current `fillColor` if provided
let textRangeColor: CGColor
if let fillColor {
if let (r, g, b) = fillColor.rgb {
textRangeColor = .rgba(r, g, b, selectedRangeOpacity)
} else {
LottieLogger.shared.warn("Could not convert color \(fillColor) to RGB values.")
textRangeColor = .rgba(0, 0, 0, selectedRangeOpacity)
}
} else {
textRangeColor = .rgba(0, 0, 0, selectedRangeOpacity)
}
attrString.addAttribute(
NSAttributedString.Key.foregroundColor,
value: textRangeColor,
range: NSRange(location: startIndex, length: endIndex - startIndex))
}
let attrString = NSAttributedString(string: text, attributes: attributes)
attributedString = attrString
if fillColor != nil {
......
......@@ -119,17 +119,17 @@ class LayerTransformNode: AnimatorNode {
func rebuildOutputs(frame _: CGFloat) {
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
let position: CGPoint =
if let point = transformProperties.position?.value.pointValue {
point
} else if
let xPos = transformProperties.positionX?.value.cgFloatValue,
let yPos = transformProperties.positionY?.value.cgFloatValue
{
CGPoint(x: xPos, y: yPos)
} else {
.zero
}
let position: CGPoint
if let point = transformProperties.position?.value.pointValue {
position = point
} else if
let xPos = transformProperties.positionX?.value.cgFloatValue,
let yPos = transformProperties.positionY?.value.cgFloatValue
{
position = CGPoint(x: xPos, y: yPos)
} else {
position = .zero
}
localTransform = CATransform3D.makeTransform(
anchor: transformProperties.anchor.value.pointValue,
......
......@@ -49,12 +49,10 @@ extension [ShapeItem] {
switch star.starType {
case .none:
continue
case .polygon:
let node = PolygonNode(parentNode: nodeTree.rootNode, star: star)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
case .star:
let node = StarNode(parentNode: nodeTree.rootNode, star: star)
nodeTree.rootNode = node
......
// Created by Lan Xu on 2024/6/7.
// Copyright © 2024 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - DropShadowNode
final class DropShadowNode: LayerEffectNode {
// MARK: Lifecycle
init(model: DropShadowModel) {
properties = DropShadowNodeProperties(model: model)
}
// MARK: Internal
var properties: DropShadowNodeProperties
var propertyMap: any NodePropertyMap {
properties
}
func applyEffect(to layer: CALayer) {
if let opacity = properties.opacity?.value.cgFloatValue {
// Lottie animation files express opacity as a numerical percentage value
// (e.g. 0%, 50%, 100%) so we divide by 100 to get the decimal values
// expected by Core Animation (e.g. 0.0, 0.5, 1.0).
layer.shadowOpacity = Float(opacity / 100)
}
if
let angleDegrees = properties.angle?.value.cgFloatValue,
let distance = properties.distance?.value.cgFloatValue
{
// Lottie animation files express rotation in degrees
// (e.g. 90º, 180º, 360º) so we convert to radians to get the
// values expected by Core Animation (e.g. π/2, π, 2π)
let angleRadians = (angleDegrees * .pi) / 180
// Lottie animation files express the `shadowOffset` as (angle, distance) pair,
// which we convert to the expected x / y offset values:
let offsetX = distance * cos(angleRadians)
let offsetY = distance * sin(angleRadians)
layer.shadowOffset = .init(width: offsetX, height: offsetY)
}
layer.shadowColor = properties.color?.value.cgColorValue
layer.shadowRadius = properties.radius?.value.cgFloatValue ?? 0
}
}
// MARK: - DropShadowNodeProperties
final class DropShadowNodeProperties: NodePropertyMap {
// MARK: Lifecycle
init(model: DropShadowModel) {
if let opacityKeyframes = model._opacity?.keyframes {
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: opacityKeyframes))
propertyMap[PropertyName.opacity.rawValue] = opacity
} else {
opacity = nil
}
if let radiusKeyframes = model._radius?.keyframes {
radius = NodeProperty(provider: KeyframeInterpolator(keyframes: radiusKeyframes))
propertyMap["Radius"] = radius
} else {
radius = nil
}
if let colorKeyFrames = model._color?.keyframes {
color = NodeProperty(provider: KeyframeInterpolator(keyframes: colorKeyFrames))
propertyMap[PropertyName.color.rawValue] = color
} else {
color = nil
}
if let angleKeyFrames = model._angle?.keyframes {
angle = NodeProperty(provider: KeyframeInterpolator(keyframes: angleKeyFrames))
propertyMap["Angle"] = angle
} else {
angle = nil
}
if let distanceKeyFrame = model._distance?.keyframes {
distance = NodeProperty(provider: KeyframeInterpolator(keyframes: distanceKeyFrame))
propertyMap["Distance"] = distance
} else {
distance = nil
}
properties = Array(propertyMap.values)
}
// MARK: Internal
var propertyMap: [String: AnyNodeProperty] = [:]
var properties: [AnyNodeProperty]
let opacity: NodeProperty<LottieVector1D>?
let radius: NodeProperty<LottieVector1D>?
let color: NodeProperty<LottieColor>?
let angle: NodeProperty<LottieVector1D>?
let distance: NodeProperty<LottieVector1D>?
}
// Created by Lan Xu on 2024/6/8.
// Copyright © 2024 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - LayerEffectNode
protocol LayerEffectNode {
func applyEffect(to layer: CALayer)
var propertyMap: NodePropertyMap { get }
}
extension LayerEffectNode {
func updateWithFrame(layer: CALayer, frame: CGFloat) {
for property in propertyMap.properties {
if property.needsUpdate(frame: frame) {
property.update(frame: frame)
}
}
applyEffect(to: layer)
}
}
......@@ -11,18 +11,18 @@ extension FillRule {
var cgFillRule: CGPathFillRule {
switch self {
case .evenOdd:
.evenOdd
return .evenOdd
default:
.winding
return .winding
}
}
var caFillRule: CAShapeLayerFillRule {
switch self {
case .evenOdd:
CAShapeLayerFillRule.evenOdd
return CAShapeLayerFillRule.evenOdd
default:
CAShapeLayerFillRule.nonZero
return CAShapeLayerFillRule.nonZero
}
}
}
......
......@@ -11,26 +11,26 @@ extension LineJoin {
var cgLineJoin: CGLineJoin {
switch self {
case .bevel:
.bevel
return .bevel
case .none:
.miter
return .miter
case .miter:
.miter
return .miter
case .round:
.round
return .round
}
}
var caLineJoin: CAShapeLayerLineJoin {
switch self {
case .none:
CAShapeLayerLineJoin.miter
return CAShapeLayerLineJoin.miter
case .miter:
CAShapeLayerLineJoin.miter
return CAShapeLayerLineJoin.miter
case .round:
CAShapeLayerLineJoin.round
return CAShapeLayerLineJoin.round
case .bevel:
CAShapeLayerLineJoin.bevel
return CAShapeLayerLineJoin.bevel
}
}
}
......@@ -39,26 +39,26 @@ extension LineCap {
var cgLineCap: CGLineCap {
switch self {
case .none:
.butt
return .butt
case .butt:
.butt
return .butt
case .round:
.round
return .round
case .square:
.square
return .square
}
}
var caLineCap: CAShapeLayerLineCap {
switch self {
case .none:
CAShapeLayerLineCap.butt
return CAShapeLayerLineCap.butt
case .butt:
CAShapeLayerLineCap.butt
return CAShapeLayerLineCap.butt
case .round:
CAShapeLayerLineCap.round
return CAShapeLayerLineCap.round
case .square:
CAShapeLayerLineCap.square
return CAShapeLayerLineCap.square
}
}
}
......
......@@ -122,7 +122,7 @@ extension BezierPath {
var vertices = [CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero)]
var previousPoint = point
currentAngle += anglePerPoint
currentAngle += anglePerPoint;
for _ in 0..<Int(ceil(numberOfPoints)) {
previousPoint = point
point = CGPoint(
......@@ -131,8 +131,8 @@ extension BezierPath {
if outerRoundedness != 0 {
let cp1Theta = (atan2(previousPoint.y, previousPoint.x) - CGFloat.pi / 2)
let cp1Dx = cos(cp1Theta)
let cp1Dy = sin(cp1Theta)
let cp1Dx = cos(cp1Theta);
let cp1Dy = sin(cp1Theta);
let cp2Theta = (atan2(point.y, point.x) - CGFloat.pi / 2)
let cp2Dx = cos(cp2Theta)
......@@ -154,7 +154,7 @@ extension BezierPath {
} else {
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
}
currentAngle += anglePerPoint
currentAngle += anglePerPoint;
}
let reverse = direction == .counterClockwise
if reverse {
......
......@@ -169,11 +169,11 @@ extension [DashElement] {
}
extension [CGFloat] {
/// If all of the items in the dash pattern are zeros, then we shouldn't attempt to render it.
/// This causes Core Animation to have extremely poor performance for some reason, even though
/// it doesn't affect the appearance of the animation.
/// - We check for `== 0.01` instead of `== 0` because `dashPattern.shapeLayerConfiguration`
/// converts all `0` values to `0.01` to work around a different Core Animation rendering issue.
// If all of the items in the dash pattern are zeros, then we shouldn't attempt to render it.
// This causes Core Animation to have extremely poor performance for some reason, even though
// it doesn't affect the appearance of the animation.
// - We check for `== 0.01` instead of `== 0` because `dashPattern.shapeLayerConfiguration`
// converts all `0` values to `0.01` to work around a different Core Animation rendering issue.
var isSupportedLayerDashPattern: Bool {
!allSatisfy { $0 == 0.01 }
}
......
......@@ -15,7 +15,6 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
init(textAnimator: TextAnimator) {
keypathName = textAnimator.name
textRangeUnit = textAnimator.textRangeUnit
var properties = [String : AnyNodeProperty]()
if let keyframeGroup = textAnimator.anchor {
......@@ -110,27 +109,6 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
tracking = nil
}
if let startKeyframes = textAnimator.start {
start = NodeProperty(provider: KeyframeInterpolator(keyframes: startKeyframes.keyframes))
properties["Start"] = start
} else {
start = nil
}
if let endKeyframes = textAnimator.end {
end = NodeProperty(provider: KeyframeInterpolator(keyframes: endKeyframes.keyframes))
properties["End"] = end
} else {
end = nil
}
if let selectedRangeOpacityKeyframes = textAnimator.opacity {
selectedRangeOpacity = NodeProperty(provider: KeyframeInterpolator(keyframes: selectedRangeOpacityKeyframes.keyframes))
properties["SelectedRangeOpacity"] = selectedRangeOpacity
} else {
selectedRangeOpacity = nil
}
keypathProperties = properties
self.properties = Array(keypathProperties.values)
......@@ -153,10 +131,6 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
let fillColor: NodeProperty<LottieColor>?
let strokeWidth: NodeProperty<LottieVector1D>?
let tracking: NodeProperty<LottieVector1D>?
let start: NodeProperty<LottieVector1D>?
let end: NodeProperty<LottieVector1D>?
let selectedRangeOpacity: NodeProperty<LottieVector1D>?
let textRangeUnit: TextRangeUnit?
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
......@@ -249,33 +223,6 @@ final class TextOutputNode: NodeOutput {
}
}
var start: CGFloat? {
get {
_start
}
set {
_start = newValue
}
}
var end: CGFloat? {
get {
_end
}
set {
_end = newValue
}
}
var selectedRangeOpacity: CGFloat? {
get {
_selectedRangeOpacity
}
set {
_selectedRangeOpacity = newValue
}
}
func hasOutputUpdates(_: CGFloat) -> Bool {
// TODO Fix This
true
......@@ -289,9 +236,6 @@ final class TextOutputNode: NodeOutput {
fileprivate var _fillColor: CGColor?
fileprivate var _tracking: CGFloat?
fileprivate var _strokeWidth: CGFloat?
fileprivate var _start: CGFloat?
fileprivate var _end: CGFloat?
fileprivate var _selectedRangeOpacity: CGFloat?
}
// MARK: - TextAnimatorNode
......@@ -334,13 +278,10 @@ class TextAnimatorNode: AnimatorNode {
func rebuildOutputs(frame _: CGFloat) {
textOutputNode.xform = textAnimatorProperties.caTransform
textOutputNode.opacity = 1.0
textOutputNode.opacity = (textAnimatorProperties.opacity?.value.cgFloatValue ?? 100) * 0.01
textOutputNode.strokeColor = textAnimatorProperties.strokeColor?.value.cgColorValue
textOutputNode.fillColor = textAnimatorProperties.fillColor?.value.cgColorValue
textOutputNode.tracking = textAnimatorProperties.tracking?.value.cgFloatValue ?? 1
textOutputNode.strokeWidth = textAnimatorProperties.strokeWidth?.value.cgFloatValue ?? 0
textOutputNode.start = textAnimatorProperties.start?.value.cgFloatValue
textOutputNode.end = textAnimatorProperties.end?.value.cgFloatValue
textOutputNode.selectedRangeOpacity = (textAnimatorProperties.opacity?.value.cgFloatValue).flatMap { $0 * 0.01 }
}
}
......@@ -29,9 +29,9 @@ struct DotLottieAnimation: Codable {
var loopMode: LottieLoopMode {
switch mode {
case .bounce:
.autoReverse
return .autoReverse
case .normal, nil:
(loop ?? false) ? .loop : .playOnce
return (loop ?? false) ? .loop : .playOnce
}
}
......
......@@ -15,7 +15,10 @@ enum DotLottieUtils {
/// Temp folder to app directory
static var tempDirectoryURL: URL {
FileManager.default.temporaryDirectory
if #available(iOS 10.0, macOS 10.12, *) {
return FileManager.default.temporaryDirectory
}
return URL(fileURLWithPath: NSTemporaryDirectory())
}
}
......@@ -53,8 +56,7 @@ public enum DotLottieError: Error {
case noDataLoaded
/// Asset with this name was not found in the provided bundle.
case assetNotFound(name: String, bundle: Bundle?)
/// Animation loading from asset was not supported on macOS 10.10,
/// but this error is no longer used.
/// Animation loading from asset is not supported on macOS 10.10.
case loadingFromAssetNotSupported
@available(*, deprecated, message: "Unused")
......
......@@ -151,15 +151,12 @@ extension KeyframeGroup: DictionaryInitializable where T: AnyInitializable {
{
keyframes = [Keyframe<T>(value)]
} else {
let frameDictionaries: [[String: Any]] =
if
let singleFrameDictionary =
dictionary[KeyframeWrapperKey.keyframeData.rawValue] as? [String: Any]
{
[singleFrameDictionary]
} else {
try dictionary.value(for: KeyframeWrapperKey.keyframeData)
}
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<T>?
for frameDictionary in frameDictionaries {
let data = try KeyframeData<T>(dictionary: frameDictionary)
......
......@@ -23,14 +23,14 @@ extension EffectValueType: ClassFamily {
func getType() -> AnyObject.Type {
switch self {
case .slider:
Vector1DEffectValue.self
return Vector1DEffectValue.self
case .angle:
Vector1DEffectValue.self
return Vector1DEffectValue.self
case .color:
ColorEffectValue.self
return ColorEffectValue.self
case .unknown:
// Unsupported
LayerEffect.self
return LayerEffect.self
}
}
}
......
......@@ -21,10 +21,10 @@ extension LayerEffectType: ClassFamily {
func getType() -> AnyObject.Type {
switch self {
case .dropShadow:
DropShadowEffect.self
return DropShadowEffect.self
case .unknown:
// Unsupported
LayerEffect.self
return LayerEffect.self
}
}
}
......
......@@ -20,10 +20,10 @@ extension LayerStyleType: ClassFamily {
func getType() -> AnyObject.Type {
switch self {
case .dropShadow:
DropShadowStyle.self
return DropShadowStyle.self
case .unknown:
// Unsupported
LayerStyle.self
return LayerStyle.self
}
}
}
......
......@@ -14,19 +14,19 @@ extension LayerType: ClassFamily {
func getType() -> AnyObject.Type {
switch self {
case .precomp:
PreCompLayerModel.self
return PreCompLayerModel.self
case .solid:
SolidLayerModel.self
return SolidLayerModel.self
case .image:
ImageLayerModel.self
return ImageLayerModel.self
case .null:
LayerModel.self
return LayerModel.self
case .shape:
ShapeLayerModel.self
return ShapeLayerModel.self
case .text:
TextLayerModel.self
return TextLayerModel.self
case .unknown:
LayerModel.self
return LayerModel.self
}
}
}
......
......@@ -38,35 +38,35 @@ extension ShapeType: ClassFamily {
func getType() -> AnyObject.Type {
switch self {
case .ellipse:
Ellipse.self
return Ellipse.self
case .fill:
Fill.self
return Fill.self
case .gradientFill:
GradientFill.self
return GradientFill.self
case .group:
Group.self
return Group.self
case .gradientStroke:
GradientStroke.self
return GradientStroke.self
case .merge:
Merge.self
return Merge.self
case .rectangle:
Rectangle.self
return Rectangle.self
case .repeater:
Repeater.self
return Repeater.self
case .round:
RoundedCorners.self
return RoundedCorners.self
case .shape:
Shape.self
return Shape.self
case .star:
Star.self
return Star.self
case .stroke:
Stroke.self
return Stroke.self
case .trim:
Trim.self
return Trim.self
case .transform:
ShapeTransform.self
return ShapeTransform.self
default:
ShapeItem.self
return ShapeItem.self
}
}
}
......
......@@ -5,15 +5,6 @@
// Created by Brandon Withrow on 1/9/19.
//
// MARK: - TextRangeUnit
enum TextRangeUnit: Int, RawRepresentable, Codable {
case percentage = 1
case index = 2
}
// MARK: - TextAnimator
final class TextAnimator: Codable, DictionaryInitializable {
// MARK: Lifecycle
......@@ -21,7 +12,6 @@ final class TextAnimator: Codable, DictionaryInitializable {
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TextAnimator.CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
let animatorContainer = try container.nestedContainer(keyedBy: TextAnimatorKeys.self, forKey: .textAnimator)
fillColor = try animatorContainer.decodeIfPresent(KeyframeGroup<LottieColor>.self, forKey: .fillColor)
strokeColor = try animatorContainer.decodeIfPresent(KeyframeGroup<LottieColor>.self, forKey: .strokeColor)
......@@ -42,16 +32,10 @@ final class TextAnimator: Codable, DictionaryInitializable {
rotationZ = nil
}
opacity = try animatorContainer.decodeIfPresent(KeyframeGroup<LottieVector1D>.self, forKey: .opacity)
let selectorContainer = try? container.nestedContainer(keyedBy: TextSelectorKeys.self, forKey: .textSelector)
start = try? selectorContainer?.decodeIfPresent(KeyframeGroup<LottieVector1D>.self, forKey: .start)
end = try? selectorContainer?.decodeIfPresent(KeyframeGroup<LottieVector1D>.self, forKey: .end)
textRangeUnit = try? selectorContainer?.decodeIfPresent(TextRangeUnit.self, forKey: .textRangeUnits)
}
init(dictionary: [String: Any]) throws {
name = (try? dictionary.value(for: CodingKeys.name)) ?? ""
let animatorDictionary: [String: Any] = try dictionary.value(for: CodingKeys.textAnimator)
if let fillColorDictionary = animatorDictionary[TextAnimatorKeys.fillColor.rawValue] as? [String: Any] {
fillColor = try? KeyframeGroup<LottieColor>(dictionary: fillColorDictionary)
......@@ -123,26 +107,6 @@ final class TextAnimator: Codable, DictionaryInitializable {
} else {
opacity = nil
}
let selectorDictionary: [String: Any] = try dictionary.value(for: CodingKeys.textSelector)
if let startDictionary = selectorDictionary[TextSelectorKeys.start.rawValue] as? [String: Any] {
start = try KeyframeGroup<LottieVector1D>(dictionary: startDictionary)
} else {
start = nil
}
if let endDictionary = selectorDictionary[TextSelectorKeys.end.rawValue] as? [String: Any] {
end = try KeyframeGroup<LottieVector1D>(dictionary: endDictionary)
} else {
end = nil
}
if let textRangeUnitValue = selectorDictionary[TextSelectorKeys.textRangeUnits.rawValue] as? Int {
textRangeUnit = TextRangeUnit(rawValue: textRangeUnitValue)
} else {
textRangeUnit = nil
}
}
// MARK: Internal
......@@ -188,15 +152,6 @@ final class TextAnimator: Codable, DictionaryInitializable {
/// Tracking
let tracking: KeyframeGroup<LottieVector1D>?
/// Start
let start: KeyframeGroup<LottieVector1D>?
/// End
let end: KeyframeGroup<LottieVector1D>?
/// The type of unit used by the start/end ranges
let textRangeUnit: TextRangeUnit?
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var animatorContainer = container.nestedContainer(keyedBy: TextAnimatorKeys.self, forKey: .textAnimator)
......@@ -209,7 +164,7 @@ final class TextAnimator: Codable, DictionaryInitializable {
// MARK: Private
private enum CodingKeys: String, CodingKey {
case textSelector = "s"
// case textSelector = "s" TODO
case textAnimator = "a"
case name = "nm"
}
......@@ -218,7 +173,6 @@ final class TextAnimator: Codable, DictionaryInitializable {
case start = "s"
case end = "e"
case offset = "o"
case textRangeUnits = "r"
}
private enum TextAnimatorKeys: String, CodingKey {
......
......@@ -95,12 +95,12 @@ extension CALayer {
}
if visible {
let style: LayerDebugStyle =
if let layerDebugging = self as? LayerDebugging {
layerDebugging.debugStyle
} else {
LayerDebugStyle.defaultStyle()
}
let style: LayerDebugStyle
if let layerDebugging = self as? LayerDebugging {
style = layerDebugging.debugStyle
} else {
style = LayerDebugStyle.defaultStyle()
}
let debugLayer = DebugLayer(style: style)
var container = self
if let cust = self as? CustomLayerDebugging {
......
......@@ -124,12 +124,12 @@ extension KeypathSearchable {
func allKeypaths(for keyPath: AnimationKeypath? = nil) -> [String] {
var allKeypaths: [String] = []
let newKeypath: AnimationKeypath =
if let previousKeypath = keyPath {
previousKeypath.appendingKey(keypathName)
} else {
AnimationKeypath(keys: [keypathName])
}
let newKeypath: AnimationKeypath
if let previousKeypath = keyPath {
newKeypath = previousKeypath.appendingKey(keypathName)
} else {
newKeypath = AnimationKeypath(keys: [keypathName])
}
allKeypaths.append(newKeypath.fullPath)
......@@ -148,12 +148,12 @@ extension KeypathSearchable {
func layerKeypaths(for keyPath: AnimationKeypath? = nil) -> [CALayer: AnimationKeypath] {
var allKeypaths: [CALayer: AnimationKeypath] = [:]
let newKeypath: AnimationKeypath =
if let previousKeypath = keyPath {
previousKeypath.appendingKey(keypathName)
} else {
AnimationKeypath(keys: [keypathName])
}
let newKeypath: AnimationKeypath
if let previousKeypath = keyPath {
newKeypath = previousKeypath.appendingKey(keypathName)
} else {
newKeypath = AnimationKeypath(keys: [keypathName])
}
if let layer = self as? CALayer {
allKeypaths[layer] = newKeypath
......@@ -197,7 +197,7 @@ extension AnimationKeypath {
keys.joined(separator: ".")
}
/// Pops the top keypath from the stack if the keyname matches.
// Pops the top keypath from the stack if the keyname matches.
func popKey(_ keyname: String) -> AnimationKeypath? {
guard
let currentKey,
......@@ -245,11 +245,11 @@ extension String {
var keyPathType: KeyType {
switch self {
case "*":
.wildcard
return .wildcard
case "**":
.fuzzyWildcard
return .fuzzyWildcard
default:
.specific
return .specific
}
}
......
......@@ -10,22 +10,22 @@ extension BlendMode {
/// Supported compositing filters are defined here: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71
var filterName: String? {
switch self {
case .normal: nil
case .multiply: "multiplyBlendMode"
case .screen: "screenBlendMode"
case .overlay: "overlayBlendMode"
case .darken: "darkenBlendMode"
case .lighten: "lightenBlendMode"
case .colorDodge: "colorDodgeBlendMode"
case .colorBurn: "colorBurnBlendMode"
case .hardLight: "hardLightBlendMode"
case .softLight: "softLightBlendMode"
case .difference: "differenceBlendMode"
case .exclusion: "exclusionBlendMode"
case .hue: "hueBlendMode"
case .saturation: "saturationBlendMode"
case .color: "colorBlendMode"
case .luminosity: "luminosityBlendMode"
case .normal: return nil
case .multiply: return "multiplyBlendMode"
case .screen: return "screenBlendMode"
case .overlay: return "overlayBlendMode"
case .darken: return "darkenBlendMode"
case .lighten: return "lightenBlendMode"
case .colorDodge: return "colorDodgeBlendMode"
case .colorBurn: return "colorBurnBlendMode"
case .hardLight: return "hardLightBlendMode"
case .softLight: return "softLightBlendMode"
case .difference: return "differenceBlendMode"
case .exclusion: return "exclusionBlendMode"
case .hue: return "hueBlendMode"
case .saturation: return "saturationBlendMode"
case .color: return "colorBlendMode"
case .luminosity: return "luminosityBlendMode"
}
}
}
......@@ -4,23 +4,6 @@
import QuartzCore
extension CGColor {
/// Retrieves the red, green, and blue color values from this `CGColor`
var rgb: (red: CGFloat, green: CGFloat, blue: CGFloat)? {
guard let components else { return nil }
switch numberOfComponents {
case 2:
return (red: components[0], green: components[0], blue: components[0])
case 3, 4:
return (red: components[0], green: components[1], blue: components[2])
default:
// Unsupported conversion
return nil
}
}
/// Initializes a `CGColor` using the given `RGB` values
static func rgb(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> CGColor {
rgba(red, green, blue, 1.0)
......@@ -39,5 +22,4 @@ extension CGColor {
colorSpace: LottieConfiguration.shared.colorSpace,
components: [red, green, blue, alpha])!
}
}
......@@ -41,19 +41,19 @@ extension CGFloat {
t = 1
} else {
// Calculate t
let a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x
let b = 3 * P0.x - 6 * P1.x + 3 * P2.x
let c = -3 * P0.x + 3 * P1.x
let d = P0.x - self
let tTemp = CGFloat.SolveCubic(a, b, c, d)
let a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x;
let b = 3 * P0.x - 6 * P1.x + 3 * P2.x;
let c = -3 * P0.x + 3 * P1.x;
let d = P0.x - self;
let tTemp = CGFloat.SolveCubic(a, b, c, d);
if tTemp == -1 {
return -1
return -1;
}
t = tTemp
}
// Calculate y from t
return (1 - t).cubed * P0.y + 3 * t * (1 - t).squared * P1.y + 3 * t.squared * (1 - t) * P2.y + t.cubed * P3.y
return (1 - t).cubed * P0.y + 3 * t * (1 - t).squared * P1.y + 3 * t.squared * (1 - t) * P2.y + t.cubed * P3.y;
}
func cubicBezier(_ t: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat {
......@@ -66,23 +66,23 @@ extension CGFloat {
return self * ttt_
+ 3.0 * c1 * tt_ * t
+ 3.0 * c2 * t_ * tt
+ end * ttt
+ end * ttt;
}
// MARK: Fileprivate
fileprivate static func SolveQuadratic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
var result = (-b + sqrt(b.squared - 4 * a * c)) / (2 * a)
var result = (-b + sqrt(b.squared - 4 * a * c)) / (2 * a);
guard !result.isInRangeOrEqual(0, 1) else {
return result
}
result = (-b - sqrt(b.squared - 4 * a * c)) / (2 * a)
result = (-b - sqrt(b.squared - 4 * a * c)) / (2 * a);
guard !result.isInRangeOrEqual(0, 1) else {
return result
}
return -1
return -1;
}
fileprivate static func SolveCubic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat {
......@@ -110,43 +110,43 @@ extension CGFloat {
var t = r - sqrt(disc)
t = (t < 0) ? -((-t).cubicRoot) : t.cubicRoot
let result = -term1 + s + t
let result = -term1 + s + t;
if result.isInRangeOrEqual(0, 1) {
return result
}
} else if disc == 0 {
let r13 = (r < 0) ? -((-r).cubicRoot) : r.cubicRoot
let r13 = (r < 0) ? -((-r).cubicRoot) : r.cubicRoot;
var result = -term1 + 2.0 * r13
var result = -term1 + 2.0 * r13;
if result.isInRangeOrEqual(0, 1) {
return result
}
result = -(r13 + term1)
result = -(r13 + term1);
if result.isInRangeOrEqual(0, 1) {
return result
}
} else {
q = -q
var dum1 = q * q * q
dum1 = acos(r / sqrt(dum1))
let r13 = 2.0 * sqrt(q)
q = -q;
var dum1 = q * q * q;
dum1 = acos(r / sqrt(dum1));
let r13 = 2.0 * sqrt(q);
var result = -term1 + r13 * cos(dum1 / 3.0)
var result = -term1 + r13 * cos(dum1 / 3.0);
if result.isInRangeOrEqual(0, 1) {
return result
}
result = -term1 + r13 * cos((dum1 + 2.0 * .pi) / 3.0)
result = -term1 + r13 * cos((dum1 + 2.0 * .pi) / 3.0);
if result.isInRangeOrEqual(0, 1) {
return result
}
result = -term1 + r13 * cos((dum1 + 4.0 * .pi) / 3.0)
result = -term1 + r13 * cos((dum1 + 4.0 * .pi) / 3.0);
if result.isInRangeOrEqual(0, 1) {
return result
}
}
return -1
return -1;
}
}
......@@ -22,12 +22,15 @@ extension Data {
throw DotLottieError.assetNotFound(name: assetName, bundle: bundle)
}
#else
if let asset = NSDataAsset(name: assetName, bundle: bundle) {
self = asset.data
return
} else {
throw DotLottieError.assetNotFound(name: assetName, bundle: bundle)
if #available(macOS 10.11, *) {
if let asset = NSDataAsset(name: assetName, bundle: bundle) {
self = asset.data
return
} else {
throw DotLottieError.assetNotFound(name: assetName, bundle: bundle)
}
}
throw DotLottieError.loadingFromAssetNotSupported
#endif
}
}
......@@ -33,6 +33,13 @@ extension CGFloat {
return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
/// Returns a value that is clamped between the two numbers
///
/// 1. The order of arguments does not matter.
func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat {
CGFloat(Double(self).clamp(Double(a), Double(b)))
}
/// Returns the difference between the receiver and the given number.
/// - Parameter absolute: If *true* (Default) the returned value will always be positive.
func diff(_ a: CGFloat, absolute: Bool = true) -> CGFloat {
......@@ -47,20 +54,20 @@ extension CGFloat {
// MARK: - Double
extension Double {
func remap(fromLow: Double, fromHigh: Double, toLow: Double, toHigh: Double) -> Double {
toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
}
extension Numeric where Self: Comparable {
/// Returns a value that is clamped between the two numbers
///
/// 1. The order of arguments does not matter.
func clamp(_ a: Self, _ b: Self) -> Self {
func clamp(_ a: Double, _ b: Double) -> Double {
let minValue = a <= b ? a : b
let maxValue = a <= b ? b : a
return max(min(self, maxValue), minValue)
}
}
extension CGRect {
......@@ -265,12 +272,12 @@ extension CGPoint {
/// Operator convenience to divide points with /
static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs))
}
/// Operator convenience to multiply points with *
static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs))
}
/// Operator convenience to add points with +
......@@ -419,7 +426,7 @@ extension CGPoint {
}
func colinear(_ a: CGPoint, _ b: CGPoint) -> Bool {
let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y)
let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y);
let accuracy: CGFloat = 0.05
if area < accuracy, area > -accuracy {
return true
......
......@@ -4,6 +4,7 @@
#if canImport(SwiftUI)
import SwiftUI
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension Binding {
/// Helper to transform a `Binding` from one `Value` type to another.
......
......@@ -5,31 +5,18 @@
import Combine
import SwiftUI
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension View {
/// A backwards compatible wrapper for iOS 14 `onChange`
@ViewBuilder
func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
#if compiler(>=5.9)
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, visionOS 1.0, *) {
self.onChange(of: value) { _, newValue in
onChange(newValue)
}
} else if #available(iOS 14.0, macOS 11.0, tvOS 14.0, *) {
self.onChange(of: value, perform: onChange)
} else {
onReceive(Just(value)) { value in
onChange(value)
}
}
#else
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, *) {
if #available(iOS 14.0, *, macOS 11.0, tvOS 14.0) {
self.onChange(of: value, perform: onChange)
} else {
onReceive(Just(value)) { value in
onChange(value)
}
}
#endif
}
}
#endif
......@@ -16,7 +16,7 @@ extension LottieColor {
init(h: Double, s: Double, v: Double, a: Double) {
let i = floor(h * 6)
let f = h * 6 - i
let p = v * (1 - s)
let p = v * (1 - s);
let q = v * (1 - f * s)
let t = v * (1 - (1 - f) * s)
......@@ -25,32 +25,26 @@ extension LottieColor {
r = v
g = t
b = p
case 1:
r = q
g = v
b = p
case 2:
r = p
g = v
b = t
case 3:
r = p
g = q
b = v
case 4:
r = t
g = p
b = v
case 5:
r = v
g = p
b = q
default:
r = 0
g = 0
......@@ -77,10 +71,10 @@ extension LottieColor {
var h: Double, s: Double, v: Double = maxValue
let d = maxValue - minValue
s = maxValue == 0 ? 0 : d / maxValue
s = maxValue == 0 ? 0 : d / maxValue;
if maxValue == minValue {
h = 0 // achromatic
h = 0; // achromatic
} else {
switch maxValue {
case r: h = (g - b) / d + (g < b ? 6 : 0)
......
......@@ -19,9 +19,9 @@ extension LottieAnimationSource {
var animation: LottieAnimation? {
switch self {
case .lottieAnimation(let animation):
animation
return animation
case .dotLottieFile:
dotLottieAnimation?.animation
return dotLottieAnimation?.animation
}
}
......@@ -29,9 +29,9 @@ extension LottieAnimationSource {
var dotLottieAnimation: DotLottieFile.Animation? {
switch self {
case .lottieAnimation:
nil
return nil
case .dotLottieFile(let dotLottieFile):
dotLottieFile.animation()
return dotLottieFile.animation()
}
}
}
......
......@@ -343,14 +343,7 @@ extension BezierPath: Codable {
totalLength = totalLength + pathElement.length
}
if closed {
// Don't use an out tangent for the closing point, since the
// closing point is exactly equal to the starting point.
let closeVertex = CurveVertex(
point: firstVertex.point,
inTangentRelative: firstVertex.inTangentRelative,
outTangentRelative: .zero)
let closeElement = previousElement.pathElementTo(closeVertex)
let closeElement = previousElement.pathElementTo(firstVertex)
decodedElements.append(closeElement)
totalLength = totalLength + closeElement.length
}
......@@ -455,14 +448,7 @@ extension BezierPath: AnyInitializable {
totalLength = totalLength + pathElement.length
}
if closed {
// Don't use an out tangent for the closing point, since the
// closing point is exactly equal to the starting point.
let closeVertex = CurveVertex(
point: firstVertex.point,
inTangentRelative: firstVertex.inTangentRelative,
outTangentRelative: .zero)
let closeElement = previousElement.pathElementTo(closeVertex)
let closeElement = previousElement.pathElementTo(firstVertex)
decodedElements.append(closeElement)
totalLength = totalLength + closeElement.length
}
......
......@@ -30,7 +30,7 @@ import Foundation
// shape's vertex distances and the roundedness set in the animation.
extension CompoundBezierPath {
/// Round corners of a compound bezier
// Round corners of a compound bezier
func roundCorners(radius: CGFloat) -> CompoundBezierPath {
var newPaths = [BezierPath]()
for path in paths {
......@@ -43,7 +43,7 @@ extension CompoundBezierPath {
}
extension BezierPath {
/// Computes a new `BezierPath` with each corner rounded based on the given `radius`
// Computes a new `BezierPath` with each corner rounded based on the given `radius`
func roundCorners(radius: CGFloat) -> BezierPath {
var newPath = BezierPath()
var uniquePath = BezierPath()
......
......@@ -16,26 +16,26 @@ extension LottieColor: Codable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var r1: Double =
if !container.isAtEnd {
try container.decode(Double.self)
} else {
0
}
var g1: Double =
if !container.isAtEnd {
try container.decode(Double.self)
} else {
0
}
var b1: Double =
if !container.isAtEnd {
try container.decode(Double.self)
} else {
0
}
var r1: Double
if !container.isAtEnd {
r1 = try container.decode(Double.self)
} else {
r1 = 0
}
var g1: Double
if !container.isAtEnd {
g1 = try container.decode(Double.self)
} else {
g1 = 0
}
var b1: Double
if !container.isAtEnd {
b1 = try container.decode(Double.self)
} else {
b1 = 0
}
if r1 > 1, g1 > 1, b1 > 1 {
r1 = r1 / 255
......
......@@ -101,15 +101,15 @@ struct CompoundBezierPath {
return self
}
var positions: [(start: CGFloat, end: CGFloat)] =
if endPosition < startPosition {
[
(start: 0, end: endPosition * length),
(start: startPosition * length, end: length),
]
} else {
[(start: startPosition * length, end: endPosition * length)]
}
var positions: [(start: CGFloat, end: CGFloat)]
if endPosition < startPosition {
positions = [
(start: 0, end: endPosition * length),
(start: startPosition * length, end: length),
]
} else {
positions = [(start: startPosition * length, end: endPosition * length)]
}
var compoundPath = CompoundBezierPath()
var trim = positions.remove(at: 0)
......
......@@ -171,6 +171,7 @@ extension LottieAnimation {
/// - Parameter url: The url to load the animation from.
/// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared`. Optional.
///
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func loadedFrom(
url: URL,
session: URLSession = .shared,
......
......@@ -237,12 +237,12 @@ public class LottieAnimationLayer: CALayer {
self.loopMode = loopMode
}
let fromTime: CGFloat =
if let fromName = fromMarker, let from = markers[fromName] {
CGFloat(from.frameTime)
} else {
currentFrame
}
let fromTime: CGFloat
if let fromName = fromMarker, let from = markers[fromName] {
fromTime = CGFloat(from.frameTime)
} else {
fromTime = currentFrame
}
let playTo = playEndMarkerFrame ? CGFloat(to.frameTime) : CGFloat(to.frameTime) - 1
let context = AnimationContext(
......@@ -339,7 +339,7 @@ public class LottieAnimationLayer: CALayer {
// If we don't have any more markers to play, then the marker sequence has completed.
completion?(completed)
} else {
play(markers: followingMarkers, completion: completion)
self.play(markers: followingMarkers, completion: completion)
}
})
}
......@@ -370,7 +370,6 @@ public class LottieAnimationLayer: CALayer {
case .frame(let animationFrameTime):
currentFrame = animationFrameTime
case .time(let timeInterval):
currentTime = timeInterval
......@@ -671,9 +670,9 @@ public class LottieAnimationLayer: CALayer {
}
get {
if let animation {
animation.progressTime(forFrame: currentFrame)
return animation.progressTime(forFrame: currentFrame)
} else {
0
return 0
}
}
}
......@@ -693,9 +692,9 @@ public class LottieAnimationLayer: CALayer {
}
get {
if let animation {
animation.time(forFrame: currentFrame)
return animation.time(forFrame: currentFrame)
} else {
0
return 0
}
}
}
......@@ -993,20 +992,16 @@ public class LottieAnimationLayer: CALayer {
case .stop:
removeCurrentAnimation()
updateAnimationFrame(currentContext.playFrom)
case .pause:
removeCurrentAnimation()
case .pauseAndRestore:
currentContext.closure.ignoreDelegate = true
removeCurrentAnimation()
/// Keep the stale context around for when the app enters the foreground.
animationContext = currentContext
case .forceFinish:
removeCurrentAnimation()
updateAnimationFrame(currentContext.playTo)
case .continuePlaying:
break
}
......@@ -1122,9 +1117,9 @@ public class LottieAnimationLayer: CALayer {
fileprivate var activeAnimationName: String {
switch rootAnimationLayer?.primaryAnimationKey {
case .specific(let animationKey):
animationKey
return animationKey
case .managed, nil:
_activeAnimationName
return _activeAnimationName
}
}
......@@ -1153,15 +1148,15 @@ public class LottieAnimationLayer: CALayer {
guard let animation else {
return
}
let rootAnimationLayer: RootAnimationLayer? =
switch renderingEngine {
case .automatic:
makeAutomaticEngineLayer(for: animation)
case .specific(.coreAnimation):
makeCoreAnimationLayer(for: animation)
case .specific(.mainThread):
makeMainThreadAnimationLayer(for: animation)
}
let rootAnimationLayer: RootAnimationLayer?
switch renderingEngine {
case .automatic:
rootAnimationLayer = makeAutomaticEngineLayer(for: animation)
case .specific(.coreAnimation):
rootAnimationLayer = makeCoreAnimationLayer(for: animation)
case .specific(.mainThread):
rootAnimationLayer = makeMainThreadAnimationLayer(for: animation)
}
guard let animationLayer = rootAnimationLayer else {
return
......@@ -1264,8 +1259,8 @@ public class LottieAnimationLayer: CALayer {
}
}
/// Handles any compatibility issues with the Core Animation engine
/// by falling back to the Main Thread engine
// Handles any compatibility issues with the Core Animation engine
// by falling back to the Main Thread engine
fileprivate func automaticEngineLayerDidSetUpAnimation(_ compatibilityIssues: [CompatibilityIssue]) {
// If there weren't any compatibility issues, then there's nothing else to do
if compatibilityIssues.isEmpty {
......@@ -1467,9 +1462,9 @@ public class LottieAnimationLayer: CALayer {
private var reducedMotionMarker: Marker? {
switch configuration.reducedMotionOption.currentReducedMotionMode {
case .standardMotion:
nil
return nil
case .reducedMotion:
animation?.reducedMotionMarker
return animation?.reducedMotionMarker
}
}
......@@ -1508,15 +1503,15 @@ extension LottieLoopMode {
var caAnimationConfiguration: (repeatCount: Float, autoreverses: Bool) {
switch self {
case .playOnce:
(repeatCount: 1, autoreverses: false)
return (repeatCount: 1, autoreverses: false)
case .loop:
(repeatCount: .greatestFiniteMagnitude, autoreverses: false)
return (repeatCount: .greatestFiniteMagnitude, autoreverses: false)
case .autoReverse:
(repeatCount: .greatestFiniteMagnitude, autoreverses: true)
return (repeatCount: .greatestFiniteMagnitude, autoreverses: true)
case .repeat(let amount):
(repeatCount: amount, autoreverses: false)
return (repeatCount: amount, autoreverses: false)
case .repeatBackwards(let amount):
(repeatCount: amount, autoreverses: true)
return (repeatCount: amount, autoreverses: true)
}
}
}
......@@ -43,9 +43,9 @@ public enum LottieBackgroundBehavior {
public static func `default`(for renderingEngine: RenderingEngine) -> LottieBackgroundBehavior {
switch renderingEngine {
case .mainThread:
.pauseAndRestore
return .pauseAndRestore
case .coreAnimation:
.continuePlaying
return .continuePlaying
}
}
}
......@@ -73,13 +73,13 @@ extension LottieLoopMode: Equatable {
switch (lhs, rhs) {
case (.repeat(let lhsAmount), .repeat(let rhsAmount)),
(.repeatBackwards(let lhsAmount), .repeatBackwards(let rhsAmount)):
lhsAmount == rhsAmount
return lhsAmount == rhsAmount
case (.playOnce, .playOnce),
(.loop, .loop),
(.autoReverse, .autoReverse):
true
return true
default:
false
return false
}
}
}
......@@ -567,9 +567,9 @@ open class LottieAnimationView: LottieAnimationViewBase {
// duration and curve are captured and added to the layer. This is used in the
// layout block to animate the animationLayer's position and size.
let rect = bounds
bounds = CGRect.zero
bounds = rect
setNeedsLayout()
self.bounds = CGRect.zero
self.bounds = rect
self.setNeedsLayout()
}
}
......@@ -813,7 +813,7 @@ open class LottieAnimationView: LottieAnimationViewBase {
// MARK: Internal
/// The backing CALayer for this animation view.
// The backing CALayer for this animation view.
let lottieAnimationLayer: LottieAnimationLayer
var animationLayer: RootAnimationLayer? {
......@@ -823,7 +823,7 @@ open class LottieAnimationView: LottieAnimationViewBase {
/// Set animation name from Interface Builder
@IBInspectable var animationName: String? {
didSet {
lottieAnimationLayer.animation = animationName.flatMap { LottieAnimation.named($0, animationCache: nil)
self.lottieAnimationLayer.animation = animationName.flatMap { LottieAnimation.named($0, animationCache: nil)
}
}
}
......@@ -835,15 +835,15 @@ open class LottieAnimationView: LottieAnimationViewBase {
lottieAnimationLayer.animationLoaded = { [weak self] _, animation in
guard let self else { return }
animationLoaded?(self, animation)
invalidateIntrinsicContentSize()
setNeedsLayout()
self.animationLoaded?(self, animation)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
lottieAnimationLayer.animationLayerDidLoad = { [weak self] _, _ in
guard let self else { return }
invalidateIntrinsicContentSize()
setNeedsLayout()
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
......
......@@ -122,7 +122,6 @@ extension LottieAnimationView {
case .success(let dotLottieFile):
self.loadAnimation(animationId, from: dotLottieFile)
completion?(self, nil)
case .failure(let error):
completion?(self, error)
}
......@@ -148,7 +147,6 @@ extension LottieAnimationView {
case .success(let dotLottieFile):
self.loadAnimation(animationId, from: dotLottieFile)
completion?(self, nil)
case .failure(let error):
completion?(self, error)
}
......@@ -178,7 +176,6 @@ extension LottieAnimationView {
case .success(let lottie):
self.loadAnimation(animationId, from: lottie)
completion?(self, nil)
case .failure(let error):
completion?(self, error)
}
......@@ -206,7 +203,6 @@ extension LottieAnimationView {
case .success(let dotLottieFile):
self.loadAnimation(animationId, from: dotLottieFile)
completion?(self, nil)
case .failure(let error):
completion?(self, error)
}
......
......@@ -170,7 +170,7 @@ extension LottiePlaybackMode.PlaybackMode {
.fromProgress(nil, toProgress: toProgress, loopMode: loopMode)
}
/// Plays the animation from the current frame to the given frame.
// Plays the animation from the current frame to the given frame.
/// - Parameter toFrame: The end frame of the animation.
/// - Parameter loopMode: The loop behavior of the animation.
public static func toFrame(_ toFrame: AnimationFrameTime, loopMode: LottieLoopMode) -> Self {
......@@ -211,32 +211,32 @@ extension LottiePlaybackMode {
func loopMode(_ updatedLoopMode: LottieLoopMode) -> LottiePlaybackMode {
switch self {
case .playing(let playbackMode):
.playing(playbackMode.loopMode(updatedLoopMode))
return .playing(playbackMode.loopMode(updatedLoopMode))
case .fromProgress(let fromProgress, toProgress: let toProgress, _):
.playing(.fromProgress(
return .playing(.fromProgress(
fromProgress,
toProgress: toProgress,
loopMode: updatedLoopMode))
case .fromFrame(let fromFrame, toFrame: let toFrame, _):
.playing(.fromFrame(
return .playing(.fromFrame(
fromFrame,
toFrame: toFrame,
loopMode: updatedLoopMode))
case .fromMarker(let fromMarker, let toMarker, let playEndMarkerFrame, _):
.playing(.fromMarker(
return .playing(.fromMarker(
fromMarker,
toMarker: toMarker,
playEndMarkerFrame: playEndMarkerFrame,
loopMode: updatedLoopMode))
case .marker(let marker, _):
.playing(.marker(marker, loopMode: updatedLoopMode))
return .playing(.marker(marker, loopMode: updatedLoopMode))
case .pause, .paused, .progress(_), .time(_), .frame(_), .markers:
self
return self
}
}
}
......@@ -246,15 +246,15 @@ extension LottiePlaybackMode.PlaybackMode {
func loopMode(_ updatedLoopMode: LottieLoopMode) -> LottiePlaybackMode.PlaybackMode {
switch self {
case .fromProgress(let fromProgress, let toProgress, _):
.fromProgress(fromProgress, toProgress: toProgress, loopMode: updatedLoopMode)
return .fromProgress(fromProgress, toProgress: toProgress, loopMode: updatedLoopMode)
case .fromFrame(let fromFrame, let toFrame, _):
.fromFrame(fromFrame, toFrame: toFrame, loopMode: updatedLoopMode)
return .fromFrame(fromFrame, toFrame: toFrame, loopMode: updatedLoopMode)
case .fromMarker(let fromMarker, let toMarker, let playEndMarkerFrame, _):
.fromMarker(fromMarker, toMarker: toMarker, playEndMarkerFrame: playEndMarkerFrame, loopMode: updatedLoopMode)
return .fromMarker(fromMarker, toMarker: toMarker, playEndMarkerFrame: playEndMarkerFrame, loopMode: updatedLoopMode)
case .marker(let marker, _):
.marker(marker, loopMode: updatedLoopMode)
return .marker(marker, loopMode: updatedLoopMode)
case .markers:
self
return self
}
}
}
......@@ -7,13 +7,14 @@ import SwiftUI
// MARK: - LottieView
/// A wrapper which exposes Lottie's `LottieAnimationView` to SwiftUI
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
// MARK: Lifecycle
/// Creates a `LottieView` that displays the given animation
public init(animation: LottieAnimation?) where Placeholder == EmptyView {
localAnimation = animation.map(LottieAnimationSource.lottieAnimation)
_animationSource = State(initialValue: animation.map(LottieAnimationSource.lottieAnimation))
placeholder = nil
}
......@@ -28,7 +29,7 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
/// }
/// ```
public init(dotLottieFile: DotLottieFile?) where Placeholder == EmptyView {
localAnimation = dotLottieFile.map(LottieAnimationSource.dotLottieFile)
_animationSource = State(initialValue: dotLottieFile.map(LottieAnimationSource.dotLottieFile))
placeholder = nil
}
......@@ -106,9 +107,9 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
_ loadAnimation: @escaping () async throws -> LottieAnimationSource?,
@ViewBuilder placeholder: @escaping () -> Placeholder)
{
localAnimation = nil
self.loadAnimation = loadAnimation
self.placeholder = placeholder
_animationSource = State(initialValue: nil)
}
// MARK: Public
......@@ -125,7 +126,7 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
}
.sizing(sizing)
.configure { context in
applyCurrentAnimationConfiguration(to: context.view, in: context.container)
applyCurrentAnimationConfiguration(to: context.view)
}
.configurations(configurations)
.opacity(animationSource == nil ? 0 : 1)
......@@ -141,42 +142,9 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
}
}
/// Returns a copy of this `LottieView` updated to have the specific configuration property
/// applied to its represented `LottieAnimationView` whenever it is updated via the `updateUIView(...)`
/// or `updateNSView(...)` methods.
///
/// - note: This configuration will be applied on every SwiftUI render pass.
/// Be wary of applying heavy side-effects as configuration values.
public func configure<Property>(
_ property: ReferenceWritableKeyPath<LottieAnimationView, Property>,
to value: Property)
-> Self
{
configure { $0[keyPath: property] = value }
}
/// Returns a copy of this `LottieView` updated to have the specific configuration property
/// applied to its represented `LottieAnimationView` whenever it is updated via the `updateUIView(...)`
/// or `updateNSView(...)` methods.
///
/// - note: If the `value` is already the currently-applied configuration value, it won't be applied
public func configure<Property: Equatable>(
_ property: ReferenceWritableKeyPath<LottieAnimationView, Property>,
to value: Property)
-> Self
{
configure {
guard $0[keyPath: property] != value else { return }
$0[keyPath: property] = value
}
}
/// Returns a copy of this `LottieView` updated to have the given closure applied to its
/// represented `LottieAnimationView` whenever it is updated via the `updateUIView(…)`
/// or `updateNSView(…)` method.
///
/// - note: This configuration closure will be executed on every SwiftUI render pass.
/// Be wary of applying heavy side-effects inside it.
public func configure(_ configure: @escaping (LottieAnimationView) -> Void) -> Self {
var copy = self
copy.configurations.append { context in
......@@ -185,22 +153,14 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
return copy
}
/// Returns a copy of this view that can be resized by scaling its animation
/// to always fit the size offered by its parent.
/// Returns a copy of this view that can be resized by scaling its animation to fit the size
/// offered by its parent.
public func resizable() -> Self {
var copy = self
copy.sizing = .proposed
return copy
}
/// Returns a copy of this view that adopts the intrinsic size of the animation,
/// up to the proposed size.
public func intrinsicSize() -> Self {
var copy = self
copy.sizing = .intrinsic
return copy
}
@available(*, deprecated, renamed: "playing()", message: "Will be removed in a future major release.")
public func play() -> Self {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: .playOnce)))
......@@ -232,7 +192,7 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: loopMode)))
}
/// Returns a copy of this view playing once from the current frame to the end frame
// Returns a copy of this view playing once from the current frame to the end frame
public func playing() -> Self {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: .playOnce)))
}
......@@ -494,8 +454,7 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
// MARK: Private
private let localAnimation: LottieAnimationSource?
@State private var remoteAnimation: LottieAnimationSource?
@State private var animationSource: LottieAnimationSource?
private var playbackMode: LottiePlaybackMode?
private var animationSpeed: Double?
private var reloadAnimationTrigger: AnyEquatable?
......@@ -515,16 +474,12 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
imageProvider: AnimationImageProvider,
imageProvidersAreEqual: (AnimationImageProvider, AnimationImageProvider) -> Bool)?
private var animationSource: LottieAnimationSource? {
localAnimation ?? remoteAnimation
}
private func loadAnimationIfNecessary() {
guard let loadAnimation else { return }
Task {
do {
remoteAnimation = try await loadAnimation()
animationSource = try await loadAnimation()
} catch {
logger.warn("Failed to load asynchronous Lottie animation with error: \(error)")
}
......@@ -535,17 +490,14 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
guard loadAnimation != nil else { return }
if showPlaceholderWhileReloading {
remoteAnimation = nil
animationSource = nil
}
loadAnimationIfNecessary()
}
/// Applies playback configuration for the current animation to the `LottieAnimationView`
private func applyCurrentAnimationConfiguration(
to view: LottieAnimationView,
in container: SwiftUIMeasurementContainer<LottieAnimationView>)
{
private func applyCurrentAnimationConfiguration(to view: LottieAnimationView) {
guard let animationSource else { return }
var imageProviderConfiguration = imageProviderConfiguration
var playbackMode = playbackMode
......@@ -587,10 +539,6 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
if animationSource.animation !== view.animation {
view.loadAnimation(animationSource)
animationDidLoad?(animationSource)
// Invalidate the intrinsic size of the SwiftUI measurement container,
// since any cached measurements will be out of date after updating the animation.
container.invalidateIntrinsicContentSize()
}
if
......@@ -616,6 +564,7 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
}
}
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension View {
/// The `.overlay` modifier that uses a `ViewBuilder` is available in iOS 15+, this helper function helps us to use the same API in older OSs
......
......@@ -43,9 +43,9 @@ extension ReducedMotionOption {
public var currentReducedMotionMode: ReducedMotionMode {
switch self {
case .specific(let specificMode):
specificMode
return specificMode
case .dynamic(let optionProvider, _):
optionProvider.currentReducedMotionMode
return optionProvider.currentReducedMotionMode
}
}
}
......@@ -56,11 +56,11 @@ extension ReducedMotionOption: Hashable {
public static func ==(_ lhs: ReducedMotionOption, _ rhs: ReducedMotionOption) -> Bool {
switch (lhs, rhs) {
case (.specific(let lhsMode), .specific(let rhsMode)):
lhsMode == rhsMode
return lhsMode == rhsMode
case (.dynamic(_, let lhsDataID), .dynamic(_, dataID: let rhsDataID)):
lhsDataID == rhsDataID
return lhsDataID == rhsDataID
case (.dynamic, .specific), (.specific, .dynamic):
false
return false
}
}
......
......@@ -67,9 +67,9 @@ extension RenderingEngineOption: RawRepresentable, CustomStringConvertible {
public var rawValue: String {
switch self {
case .automatic:
"Automatic"
return "Automatic"
case .specific(let engine):
engine.rawValue
return engine.rawValue
}
}
......@@ -101,9 +101,9 @@ extension RenderingEngine: RawRepresentable, CustomStringConvertible {
public var rawValue: String {
switch self {
case .mainThread:
"Main Thread"
return "Main Thread"
case .coreAnimation:
"Core Animation"
return "Core Animation"
}
}
......
......@@ -213,8 +213,8 @@ open class AnimatedSwitch: AnimatedControl {
// For the Main Thread rendering engine, we freeze the animation at the expected final progress
// once the animation is complete. This isn't necessary on the Core Animation engine.
if finished, !(animationView.animationLayer is CoreAnimationLayer) {
animationView.currentProgress = finalProgress
if finished, !(self.animationView.animationLayer is CoreAnimationLayer) {
self.animationView.currentProgress = finalProgress
}
})
}
......
......@@ -5,6 +5,7 @@
import SwiftUI
/// A wrapper which exposes Lottie's `AnimatedButton` to SwiftUI
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public struct LottieButton: UIViewConfiguringSwiftUIView {
// MARK: Lifecycle
......
......@@ -5,6 +5,7 @@
import SwiftUI
/// A wrapper which exposes Lottie's `AnimatedSwitch` to SwiftUI
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public struct LottieSwitch: UIViewConfiguringSwiftUIView {
// MARK: Lifecycle
......
......@@ -44,7 +44,7 @@ public enum LottieNSControlState: UInt, RawRepresentable {
/// - `UIControl.Event` on iOS / tvOS and `LottieNSControlEvent` on macOS.
public typealias LottieControlEvent = LottieNSControlEvent
public struct LottieNSControlEvent: Equatable, Sendable {
public struct LottieNSControlEvent: Equatable {
// MARK: Lifecycle
......
......@@ -56,9 +56,9 @@ public final class DotLottieFile {
/// The `LottieAnimation` and `DotLottieConfiguration` for the given animation ID in this file
func animation(for id: String? = nil) -> DotLottieFile.Animation? {
if let id {
animations.first(where: { $0.configuration.id == id })
return animations.first(where: { $0.configuration.id == id })
} else {
animations.first
return animations.first
}
}
......
......@@ -118,6 +118,7 @@ extension DotLottieFile {
/// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main`
/// - Parameter subdirectory: A subdirectory in the bundle in which the lottie is located. Optional.
/// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func named(
_ name: String,
bundle: Bundle = Bundle.main,
......@@ -164,6 +165,7 @@ extension DotLottieFile {
/// Loads an DotLottie from a specific filepath.
/// - Parameter filepath: The absolute filepath of the lottie to load. EG "/User/Me/starAnimation.lottie"
/// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func loadedFrom(
filepath: String,
dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache)
......@@ -202,6 +204,7 @@ extension DotLottieFile {
/// - Parameter name: The name of the lottie file in the asset catalog. EG "StarAnimation"
/// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main`
/// - Parameter dotLottieCache: A cache for holding loaded lottie files. Defaults to `LRUDotLottieCache.sharedCache` Optional.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func asset(
named name: String,
bundle: Bundle = Bundle.main,
......@@ -267,6 +270,7 @@ extension DotLottieFile {
///
/// - Parameter url: The url to load the animation from.
/// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LRUAnimationCache.sharedCache`. Optional.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func loadedFrom(
url: URL,
session: URLSession = .shared,
......@@ -350,6 +354,7 @@ extension DotLottieFile {
/// - data: The data(`Foundation.Data`) object to load DotLottie from
/// - filename: The name of the lottie file without the lottie extension. eg. "StarAnimation"
/// - dispatchQueue: A dispatch queue used to load animations. Defaults to `DispatchQueue.global()`. Optional.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func loadedFrom(
data: Data,
filename: String,
......
......@@ -49,13 +49,13 @@ extension ValueProvider {
public var typeErasedStorage: AnyValueProviderStorage {
switch storage {
case .closure(let typedClosure):
.closure(typedClosure)
return .closure(typedClosure)
case .singleValue(let typedValue):
.singleValue(typedValue)
return .singleValue(typedValue)
case .keyframes(let keyframes):
.keyframes(
return .keyframes(
keyframes.map { keyframe in
keyframe.withValue(keyframe.value as Any)
},
......@@ -88,13 +88,13 @@ public enum ValueProviderStorage<T: AnyInterpolatable> {
func value(frame: AnimationFrameTime) -> T {
switch self {
case .singleValue(let value):
value
return value
case .closure(let closure):
closure(frame)
return closure(frame)
case .keyframes(let keyframes):
KeyframeInterpolator(keyframes: ContiguousArray(keyframes)).storage.value(frame: frame)
return KeyframeInterpolator(keyframes: ContiguousArray(keyframes)).storage.value(frame: frame)
}
}
}
......@@ -119,13 +119,13 @@ public enum AnyValueProviderStorage {
func value(frame: AnimationFrameTime) -> Any {
switch self {
case .singleValue(let value):
value
return value
case .closure(let closure):
closure(frame)
return closure(frame)
case .keyframes(_, let valueForFrame):
valueForFrame(frame)
return valueForFrame(frame)
}
}
}
......@@ -70,7 +70,7 @@ public final class GradientValueProvider: ValueProvider {
public var storage: ValueProviderStorage<[Double]> {
if let block {
.closure { [self] frame in
return .closure { [self] frame in
hasUpdate = false
let newColors = block(frame)
......@@ -80,7 +80,7 @@ public final class GradientValueProvider: ValueProvider {
return value
}
} else {
.singleValue(value)
return .singleValue(value)
}
}
......
......@@ -271,9 +271,9 @@ struct Hold<T>: Interpolatable {
func interpolate(to: Hold<T>, amount: CGFloat) -> Hold<T> {
if amount < 1 {
self
return self
} else {
to
return to
}
}
}
......@@ -15,11 +15,11 @@ public enum ColorFormatDenominator: Hashable {
var value: Double {
switch self {
case .One:
1.0
return 1.0
case .OneHundred:
100.0
return 100.0
case .TwoFiftyFive:
255.0
return 255.0
}
}
}
......
......@@ -62,22 +62,22 @@ public final class DictionaryTextProvider: AnimationKeypathTextProvider, LegacyA
public func text(for keypath: AnimationKeypath, sourceText: String) -> String? {
if let valueForFullKeypath = values[keypath.fullPath] {
valueForFullKeypath
return valueForFullKeypath
}
else if
let lastKeypathComponent = keypath.keys.last,
let valueForLastComponent = values[lastKeypathComponent]
{
valueForLastComponent
return valueForLastComponent
}
else {
sourceText
return sourceText
}
}
/// Never called directly by Lottie, but we continue to implement this conformance for backwards compatibility.
// Never called directly by Lottie, but we continue to implement this conformance for backwards compatibility.
public func textFor(keypathName: String, sourceText: String) -> String {
values[keypathName] ?? sourceText
}
......
......@@ -77,15 +77,15 @@ public enum CompatibleRenderingEngineOption: Int {
{
switch configuration {
case .shared:
LottieConfiguration.shared
return LottieConfiguration.shared
case .defaultEngine:
LottieConfiguration(renderingEngine: .coreAnimation)
return LottieConfiguration(renderingEngine: .coreAnimation)
case .automatic:
LottieConfiguration(renderingEngine: .automatic)
return LottieConfiguration(renderingEngine: .automatic)
case .mainThread:
LottieConfiguration(renderingEngine: .mainThread)
return LottieConfiguration(renderingEngine: .mainThread)
case .coreAnimation:
LottieConfiguration(renderingEngine: .coreAnimation)
return LottieConfiguration(renderingEngine: .coreAnimation)
}
}
}
......@@ -219,8 +219,7 @@ public final class CompatibleAnimationView: UIView {
}
}
@objc
public override var contentMode: UIView.ContentMode {
@objc public override var contentMode: UIView.ContentMode {
set { animationView.contentMode = newValue }
get { animationView.contentMode }
}
......@@ -286,15 +285,15 @@ public final class CompatibleAnimationView: UIView {
get {
switch animationView.backgroundBehavior {
case .stop:
.stop
return .stop
case .pause:
.pause
return .pause
case .pauseAndRestore:
.pauseAndRestore
return .pauseAndRestore
case .forceFinish:
.forceFinish
return .forceFinish
case .continuePlaying:
.continuePlaying
return .continuePlaying
}
}
set {
......@@ -441,7 +440,7 @@ public final class CompatibleAnimationView: UIView {
public func getColorValue(for keypath: CompatibleAnimationKeypath, atFrame: CGFloat) -> UIColor? {
let value = animationView.getValue(for: keypath.animationKeypath, atFrame: atFrame)
guard let colorValue = value as? LottieColor else {
return nil
return nil;
}
return UIColor(
......
......@@ -39,7 +39,11 @@ open class LottieAnimationViewBase: UIView {
var screenScale: CGFloat {
#if os(iOS) || os(tvOS)
max(UITraitCollection.current.displayScale, 1)
if #available(iOS 13.0, tvOS 13.0, *) {
return max(UITraitCollection.current.displayScale, 1)
} else {
return UIScreen.main.scale
}
#else // if os(visionOS)
// We intentionally don't check `#if os(visionOS)`, because that emits
// a warning when building on Xcode 14 and earlier.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册