Skip to content

Commit

Permalink
Fix issue where LottieView animation would restart from beginning aft…
Browse files Browse the repository at this point in the history
…er backgrounding app (airbnb#2237)
  • Loading branch information
calda authored and Igor Moroz committed May 22, 2024
1 parent 77a7c1a commit fd0e234
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 15 deletions.
20 changes: 20 additions & 0 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@
08AB055D2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */; };
08AB055E2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */; };
08AB055F2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */; };
08BCA8472B0E8E0B00594EEB /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BCA8452B0E8E0B00594EEB /* LRUCache.swift */; };
08BCA8482B0E8E0B00594EEB /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BCA8452B0E8E0B00594EEB /* LRUCache.swift */; };
08BCA8492B0E8E0B00594EEB /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BCA8452B0E8E0B00594EEB /* LRUCache.swift */; };
08BCA84A2B0E8E0B00594EEB /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BCA8452B0E8E0B00594EEB /* LRUCache.swift */; };
08C001F32A46150D00AB54BA /* Archive+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C001E02A46150D00AB54BA /* Archive+Helpers.swift */; };
08C001F42A46150D00AB54BA /* Archive+MemoryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C001E12A46150D00AB54BA /* Archive+MemoryFile.swift */; };
08C001F52A46150D00AB54BA /* Archive+BackingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C001E22A46150D00AB54BA /* Archive+BackingConfiguration.swift */; };
Expand Down Expand Up @@ -1181,6 +1185,8 @@
08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReducedMotionOption.swift; sourceTree = "<group>"; };
08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingStrategy.swift; sourceTree = "<group>"; };
08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingEngineOption.swift; sourceTree = "<group>"; };
08BCA8452B0E8E0B00594EEB /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
08BCA8462B0E8E0B00594EEB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
08C001E02A46150D00AB54BA /* Archive+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Archive+Helpers.swift"; sourceTree = "<group>"; };
08C001E12A46150D00AB54BA /* Archive+MemoryFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Archive+MemoryFile.swift"; sourceTree = "<group>"; };
08C001E22A46150D00AB54BA /* Archive+BackingConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Archive+BackingConfiguration.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1553,12 +1559,22 @@
path = Configuration;
sourceTree = "<group>";
};
08BCA8442B0E8E0B00594EEB /* LRUCache */ = {
isa = PBXGroup;
children = (
08BCA8452B0E8E0B00594EEB /* LRUCache.swift */,
08BCA8462B0E8E0B00594EEB /* README.md */,
);
path = LRUCache;
sourceTree = "<group>";
};
08C001DE2A4614CF00AB54BA /* EmbeddedLibraries */ = {
isa = PBXGroup;
children = (
08C002062A46152200AB54BA /* README.md */,
08C0021E2A46166400AB54BA /* EpoxyCore */,
08C001DF2A46150D00AB54BA /* ZipFoundation */,
08BCA8442B0E8E0B00594EEB /* LRUCache */,
);
path = EmbeddedLibraries;
sourceTree = "<group>";
Expand Down Expand Up @@ -2876,6 +2892,7 @@
080DEFFE2A95712400BE2D96 /* GradientAnimations.swift in Sources */,
080DF03C2A95715900BE2D96 /* Star.swift in Sources */,
080DEFD32A95711400BE2D96 /* Entry+Serialization.swift in Sources */,
08BCA84A2B0E8E0B00594EEB /* LRUCache.swift in Sources */,
080DF01D2A95713B00BE2D96 /* StarNode.swift in Sources */,
080DEFB02A9570FE00BE2D96 /* AnimatedProviding.swift in Sources */,
080DEFC72A95710F00BE2D96 /* EpoxyableView.swift in Sources */,
Expand Down Expand Up @@ -3175,6 +3192,7 @@
2E9C96242822F43100677516 /* KeyframeGroup.swift in Sources */,
2E9C96FF2822F43100677516 /* BaseAnimationLayer.swift in Sources */,
08E206F72A56014E002DCE17 /* Collection+Diff.swift in Sources */,
08BCA8472B0E8E0B00594EEB /* LRUCache.swift in Sources */,
08C001F62A46150D00AB54BA /* Archive.swift in Sources */,
2E9C96AB2822F43100677516 /* GradientStrokeNode.swift in Sources */,
08E2071B2A56014E002DCE17 /* EpoxySwiftUIHostingView.swift in Sources */,
Expand Down Expand Up @@ -3488,6 +3506,7 @@
2EAF5AC327A0798700E00531 /* BundleImageProvider.swift in Sources */,
08AB055A2A61C5B700DE86FD /* DecodingStrategy.swift in Sources */,
08E207492A56014E002DCE17 /* AnimatedProviding.swift in Sources */,
08BCA8482B0E8E0B00594EEB /* LRUCache.swift in Sources */,
2E9C976C2822F43100677516 /* InterpolatableExtensions.swift in Sources */,
2E9C96EE2822F43100677516 /* ShapeItemLayer.swift in Sources */,
08C002D62A46196300AB54BA /* Data+CompressionDeprecated.swift in Sources */,
Expand Down Expand Up @@ -3775,6 +3794,7 @@
2E9C97012822F43100677516 /* BaseAnimationLayer.swift in Sources */,
2E9C96AD2822F43100677516 /* GradientStrokeNode.swift in Sources */,
08E206F92A56014E002DCE17 /* Collection+Diff.swift in Sources */,
08BCA8492B0E8E0B00594EEB /* LRUCache.swift in Sources */,
2EAF5AC427A0798700E00531 /* BundleImageProvider.swift in Sources */,
08E2071D2A56014E002DCE17 /* EpoxySwiftUIHostingView.swift in Sources */,
2E9C976D2822F43100677516 /* InterpolatableExtensions.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ let package = Package(
"Private/EmbeddedLibraries/README.md",
"Private/EmbeddedLibraries/ZipFoundation/README.md",
"Private/EmbeddedLibraries/EpoxyCore/README.md",
"Private/EmbeddedLibraries/LRUCache/README.md",
]),
])
256 changes: 256 additions & 0 deletions Sources/Private/EmbeddedLibraries/LRUCache/LRUCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
//
// LRUCache.swift
// LRUCache
//
// Version 1.0.2
//
// Created by Nick Lockwood on 05/08/2021.
// Copyright © 2021 Nick Lockwood. All rights reserved.
//
// Distributed under the permissive MIT license
// Get the latest version from here:
//
// https://github.com/nicklockwood/LRUCache
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

import Foundation

#if os(iOS) || os(tvOS)
import UIKit

/// Notification that cache should be cleared
let LRUCacheMemoryWarningNotification: NSNotification.Name =
UIApplication.didReceiveMemoryWarningNotification

#else

/// Notification that cache should be cleared
let LRUCacheMemoryWarningNotification: NSNotification.Name =
.init("LRUCacheMemoryWarningNotification")

#endif

// MARK: - LRUCache

final class LRUCache<Key: Hashable, Value> {

// MARK: Lifecycle

/// Initialize the cache with the specified `totalCostLimit` and `countLimit`
init(
totalCostLimit: Int = .max,
countLimit: Int = .max,
notificationCenter: NotificationCenter = .default)
{
self.totalCostLimit = totalCostLimit
self.countLimit = countLimit
self.notificationCenter = notificationCenter

token = notificationCenter.addObserver(
forName: LRUCacheMemoryWarningNotification,
object: nil,
queue: nil)
{ [weak self] _ in
self?.removeAllValues()
}
}

deinit {
if let token = token {
notificationCenter.removeObserver(token)
}
}

// MARK: Internal

/// The current total cost of values in the cache
private(set) var totalCost = 0

/// The maximum total cost permitted
var totalCostLimit: Int {
didSet { clean() }
}

/// The maximum number of values permitted
var countLimit: Int {
didSet { clean() }
}

// MARK: Private

private var values: [Key: Container] = [:]
private unowned(unsafe) var head: Container?
private unowned(unsafe) var tail: Container?
private let lock: NSLock = .init()
private var token: AnyObject?
private let notificationCenter: NotificationCenter

}

extension LRUCache {
/// The number of values currently stored in the cache
var count: Int {
values.count
}

/// Is the cache empty?
var isEmpty: Bool {
values.isEmpty
}

/// Returns all values in the cache from oldest to newest
var allValues: [Value] {
lock.lock()
defer { lock.unlock() }
var values = [Value]()
var next = head
while let container = next {
values.append(container.value)
next = container.next
}
return values
}

/// Insert a value into the cache with optional `cost`
func setValue(_ value: Value?, forKey key: Key, cost: Int = 0) {
guard let value = value else {
removeValue(forKey: key)
return
}
lock.lock()
if let container = values[key] {
container.value = value
totalCost -= container.cost
container.cost = cost
remove(container)
append(container)
} else {
let container = Container(
value: value,
cost: cost,
key: key)
values[key] = container
append(container)
}
totalCost += cost
lock.unlock()
clean()
}

/// Remove a value from the cache and return it
@discardableResult
func removeValue(forKey key: Key) -> Value? {
lock.lock()
defer { lock.unlock() }
guard let container = values.removeValue(forKey: key) else {
return nil
}
remove(container)
totalCost -= container.cost
return container.value
}

/// Fetch a value from the cache
func value(forKey key: Key) -> Value? {
lock.lock()
defer { lock.unlock() }
if let container = values[key] {
remove(container)
append(container)
return container.value
}
return nil
}

/// Remove all values from the cache
func removeAllValues() {
lock.lock()
values.removeAll()
head = nil
tail = nil
lock.unlock()
}
}

extension LRUCache {

// MARK: Fileprivate

fileprivate final class Container {

// MARK: Lifecycle

init(value: Value, cost: Int, key: Key) {
self.value = value
self.cost = cost
self.key = key
}

// MARK: Internal

var value: Value
var cost: Int
let key: Key
unowned(unsafe) var prev: Container?
unowned(unsafe) var next: Container?

}

// MARK: Private

// Remove container from list (must be called inside lock)
private func remove(_ container: Container) {
if head === container {
head = container.next
}
if tail === container {
tail = container.prev
}
container.next?.prev = container.prev
container.prev?.next = container.next
container.next = nil
}

// Append container to list (must be called inside lock)
private func append(_ container: Container) {
assert(container.next == nil)
if head == nil {
head = container
}
container.prev = tail
tail?.next = container
tail = container
}

// Remove expired values (must be called outside lock)
private func clean() {
lock.lock()
defer { lock.unlock() }
while
totalCost > totalCostLimit || count > countLimit,
let container = head
{
remove(container)
values.removeValue(forKey: container.key)
totalCost -= container.cost
}
}
}
24 changes: 24 additions & 0 deletions Sources/Private/EmbeddedLibraries/LRUCache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## LRUCache

This directory includes the source code of the LRUCache library, from the following release:
https://github.com/nicklockwood/LRUCache/releases/tag/1.0.4

Lottie is distributed via multiple package managers (SPM, Cocoapods, Carthage, and NPM),
each with different packaging and compilation requirements.

Due to limitations of these package managers, we can't depend on / import
a separate LRUCache module / library. Instead, we include the source
directly within the Lottie library and compile everything as a single unit.

### Update instructions

From time to time we may need to update to a more recent version of LRUCache.
When doing this, follow these steps:

1. Download the latest release from https://github.com/nicklockwood/LRUCache
and replace the source code in this directory with the updated code.

2. Update the URL at the top of this file to indicate what release is being used.

3. Change all of the `public` symbols defined in this module to instead be `internal`
to prevent Lottie from exposing any EpoxyCore APIs.
14 changes: 14 additions & 0 deletions Sources/Private/EmbeddedLibraries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This directory includes the source code of libraries that are embedded within lo
This includes:
- ZipFoundation (https://github.com/weichsel/ZIPFoundation)
- EpoxyCore (https://github.com/airbnb/epoxy-ios)
- LRUCache (https://github.com/nicklockwood/LRUCache)

Lottie is distributed via multiple package managers (SPM, Cocoapods, Carthage, and NPM),
each with different packaging and compilation requirements.
Expand All @@ -25,3 +26,16 @@ When doing this, follow these steps:

3. Change all of the `public` symbols defined in the module to instead be `internal`
to prevent Lottie from exposing any APIs from other libraries.

### Adding a new dependencies

1. Create a subdirectory in `EmbeddedLibraries` for the new dependency.

2. Add the dependency to the list at the top of this file.

3. Add a `README.md` to the directory for the new library, using the same formatting as the `README.md` file used by other dependencies.

4. Exclude the new `README.md` file from the lottie-ios package by adding it to the `exclude:` list in `Package.swift`.

5. Change all of the `public` symbols defined in the module to instead be `internal`
to prevent Lottie from exposing any APIs from other libraries.
Loading

0 comments on commit fd0e234

Please sign in to comment.