How Lottie Animation is Integrated in Telegram-iOS: A Technical Deep Dive
Telegram-iOS integrates Lottie animations through a dual-path architecture that uses LottieComponent and LottieAnimationComponent for UI elements, and LottieMetalAnimatedStickerNode for Metal-accelerated stickers, sharing a common resource pipeline that handles .tgs decompression via TGGUnzipData and caching through AnimationCacheState.
The Telegram Messenger iOS app leverages Lottie vector animations to power everything from animated stickers to loading spinners and transition effects. According to the source code in the TelegramMessenger/Telegram-iOS repository, the implementation splits into two specialized tracks: a ComponentFlow-based UI system for interface animations and a Metal-accelerated rendering pipeline for stickers. Both systems rely on the same underlying resource management, decompression utilities, and caching strategies to deliver smooth 60 fps performance across the supported iOS device range.
Architecture Overview
The Lottie animation integration in Telegram-iOS follows a layered architecture with distinct entry points for different use cases:
- UI Components:
LottieComponentandLottieAnimationComponentwrap Lottie animations into the app's component system, handling bundle resources, remote files, and playback modes - Animated Stickers:
LottieMetalAnimatedStickerNodeprovides Metal-accelerated rendering for.tgssticker files with pre-serialized render tree caching - Shared Pipeline: Both paths use
TGGUnzipDatafor.tgsdecompression,AnimationCacheStatefor Metal render caching, andLottieComponent.ResourceContentfor media box integration
This separation allows the UI team to add animations declaratively while the stickers team optimizes for battery efficiency through custom Metal shaders.
UI Animation Components
LottieComponent.swift
The LottieComponent class in submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift serves as the high-level wrapper for adding Lottie animations anywhere in the UI hierarchy. It abstracts file loading, decompression, and playback control through a declarative API.
The component supports multiple content sources via the Content protocol:
AppBundleContent: Loads JSON or.tgsfiles directly from the app bundleResourceContent: Streams animation data fromTelegramMediaFileobjects via the media box
Key configuration options include StartingPosition (begin, end, or fractional frame), looping behavior, and color overlays:
public final class LottieComponent: Component {
public enum StartingPosition: Equatable {
case begin
case end
case fraction(Double)
}
public init(
content: Content,
color: UIColor? = nil,
placeholderColor: UIColor? = nil,
startingPosition: StartingPosition = .end,
size: CGSize? = nil,
loop: Bool = false,
playOnce: ActionSlot<Void>? = nil
) { … }
}
LottieAnimationComponent.swift
For finer control within the ComponentFlow system, LottieAnimationComponent in submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift embeds a native Lottie AnimationView with support for tag-based matching and per-element color overrides.
The AnimationItem struct defines playback behavior through its Mode enum:
public struct AnimationItem: Equatable {
public enum StillPosition { case begin, end }
public enum Mode: Equatable {
case still(position: StillPosition)
case animating(loop: Bool)
case animateTransitionFromPrevious
}
public var name: String
public var mode: Mode
public var range: (CGFloat, CGFloat)?
public var speed: CGFloat = 1.0
public var waitForCompletion: Bool = true
}
The View.update(component:availableSize:transition:) method handles view reuse logic, loading animations either from bundle paths or by decompressing .tgs data on the fly:
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"),
let maybeAnimation = Animation.filepath(url.path) {
animation = maybeAnimation
} else if let url = getAppBundle().url(forResource: component.animation.name,
withExtension: "tgs"),
let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)),
let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
animation = try? Animation.from(data: unpackedData, strategy: .codable)
}
Practical Example: Chat Input Typing Indicator
To add a looping typing animation to a chat input field using the component system:
let typingComponent = LottieAnimationComponent(
animation: .init(name: "input_typing", mode: .animating(loop: true)),
colors: [:],
tag: nil,
size: CGSize(width: 24, height: 24)
)
// Integration in component tree
let typingNode = AnyComponent(typingComponent)
Animated Stickers and Metal Rendering
Metal-Accelerated Sticker Nodes
For animated stickers, Telegram-iOS implements LottieMetalAnimatedStickerNode in submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift. This class uses a custom Metal pipeline (LottieCpp with custom shaders) rather than the standard Lottie iOS library to achieve hardware-accelerated rendering.
The node conforms to the AnimatedStickerNode protocol and handles setup through:
public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode {
public func setup(
source: AnimatedStickerNodeSource,
width: Int,
height: Int,
playbackMode: AnimatedStickerPlaybackMode,
mode: AnimatedStickerMode
) {
// Resolves source to file path
// Requests cached binary from AnimationCacheState
// Decodes via LottieAnimationContainer
// Creates Metal render pipelines
// Drives frames via display link
}
}
Resource Loading and Caching
The Metal implementation uses AnimationCacheState to serialize render trees into binary buffers. When cacheLottieMetalAnimation(path:) is called, the system:
- Reads the
.tgsfile and decompresses viaTGGUnzipData - Builds a
LottieAnimationContainerfrom the JSON - Serializes each frame's render tree
- Queues the task (max 2 concurrent) and writes to disk for subsequent fast loads
This pre-serialization eliminates JSON parsing overhead during playback, enabling smooth 60 fps animation even on older devices.
Fallback Rendering
When Metal is unavailable, the system falls back to AnimatedStickerNode (CPU-based RLottie rendering) located in the AnimatedStickerNode framework submodule. Both implementations share the same resource handling pipeline but differ in their rendering backends.
Loading Animations from the Media Box
For custom sticker packs and remote animations, the repository provides LottieComponent.ResourceContent in submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/LottieComponentResourceContent.swift. This class bridges the gap between Telegram's media storage system and the Lottie rendering pipeline.
The load(_:) method implements a sophisticated loading strategy:
public final class ResourceContent: LottieComponent.Content {
private let context: AccountContext
private let file: TelegramMediaFile
override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
// Attempts synchronous disk read if file exists
// Subscribes to mediaBox.resourceData for download progress
// Decompresses via TGGUnzipData upon completion
// Returns animation data or placeholder
}
}
Usage example for a sticker from the media box:
let resource = LottieComponent.ResourceContent(
context: accountContext,
file: stickerFile,
attemptSynchronously: true,
providesPlaceholder: true
)
let stickerComponent = LottieComponent(
content: resource,
size: CGSize(width: 80, height: 80),
loop: false
)
The component automatically displays a placeholder while fetching the animation from Telegram's servers, then transitions to the decompressed Lottie playback once the .tgs data arrives.
Key Implementation Files
| File | Purpose | Location |
|---|---|---|
LottieComponent.swift |
High-level UI component with bundle and remote loading | submodules/TelegramUI/Components/LottieComponent/Sources/ |
LottieAnimationComponent.swift |
ComponentFlow wrapper exposing playback modes | submodules/Components/LottieAnimationComponent/Sources/ |
LottieComponentResourceContent.swift |
Media box integration for TelegramMediaFile |
submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/ |
LottieMetalAnimatedStickerNode.swift |
Metal-accelerated sticker rendering | submodules/TelegramUI/Components/LottieMetal/Sources/ |
AnimatedStickerNode.swift |
Fallback CPU-based RLottie implementation | AnimatedStickerNode submodule |
AnimationCacheState |
Render tree serialization and caching | Embedded within LottieMetalAnimatedStickerNode.swift |
Summary
- Telegram-iOS uses a dual-path architecture where
LottieComponentandLottieAnimationComponenthandle UI animations, whileLottieMetalAnimatedStickerNodemanages sticker rendering through custom Metal shaders. - Both systems share decompression logic via
TGGUnzipDatafor.tgsfiles and leverageAnimationCacheStateto cache pre-serialized render trees for optimal performance. - Resource loading adapts to source type: bundle files load synchronously while
TelegramMediaFileobjects stream through the media box with placeholder support. - The Metal implementation prioritizes 60 fps performance by serializing animation frames into binary buffers and limiting concurrent processing to 2 tasks.
Frequently Asked Questions
What is the difference between LottieComponent and LottieAnimationComponent in Telegram-iOS?
LottieComponent provides a higher-level abstraction with built-in support for placeholders, color overlays, and content sources from both app bundles and remote media files. LottieAnimationComponent offers finer-grained control within the ComponentFlow system, exposing direct access to Lottie's AnimationView, tag-based view matching, and specific frame ranges. Use LottieComponent for standard UI animations and LottieAnimationComponent when you need precise playback control or integration with complex component trees.
How does Telegram-iOS handle the .tgs file format for stickers?
The app treats .tgs files as gzipped JSON payloads. When loading a sticker, the code calls TGGUnzipData(data, 5 * 1024 * 1024) to decompress the file (with a 5MB limit), then parses the resulting JSON through Animation.from(data:strategy:) or LottieAnimationContainer depending on whether the Metal or CPU rendering path is active. The decompressed data feeds into the respective animation pipelines for frame generation.
Why does Telegram use Metal for animated stickers instead of standard Lottie?
The LottieMetalAnimatedStickerNode implementation uses custom Metal shaders and the LottieCpp framework to pre-serialize render trees into binary cache files. This approach eliminates JSON parsing during playback and enables GPU-accelerated rendering, achieving consistent 60 fps performance even on older iOS devices. The standard Lottie iOS library runs on the CPU, which consumes more battery and performs poorly with complex vector animations at sticker sizes.
How can I load a Lottie animation from a TelegramMediaFile in my own component?
Import LottieComponentResourceContent and instantiate ResourceContent with your AccountContext and TelegramMediaFile. Pass this as the content parameter when initializing LottieComponent. Set attemptSynchronously: true for instant display if cached, and providesPlaceholder: true to show a temporary view while the file downloads from Telegram's servers. The component handles the rest, including decompression and playback setup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →