Supported on: iOS SDK
1.0.61+Supported Formats
Lottie Animations
Lottie animations render natively —lottie-ios is a dependency of AppDNASDK, so there is
nothing to add to your own project.
An animation comes from a URL, and its colours are baked into the file. There is no inline
animation payload and no colour remapping: both were declared in the config models for a while,
neither renderer ever read them, and they were removed rather than implemented. A remote
.json or
.lottie keeps the config a device downloads small, and a recolour belongs in the file the designer
ships, where it can be seen before it is published.Rive Animations
Rive animations render natively too —RiveRuntime is a dependency of AppDNASDK.
Rive is supported in onboarding content blocks, paywall sections, in-app messages (both modal and
fullscreen) and server-driven screens — everywhere Lottie is except surveys.
Video
Inline video playback with native AVPlayer:- Remote URL — provide a URL to an MP4 or HLS video
- Thumbnail — optional poster image shown before playback
- Controls — autoplay, loop, muted, show/hide controls
- Sizing — configurable height and corner radius
Visual Effects
Haptic Feedback
Haptic feedback is triggered automatically based on Console configuration. Supported haptic types:
🔴 Nothing fires unless
haptic.enabled is true. The config object gates every trigger, so a
trigger set on its own does nothing — triggerIfEnabled returns early when the config is absent or
not enabled, on both platforms. A type the SDK does not recognise is also ignored rather than
falling back to a default.
The triggers, each taking one of the types above:
Particle Effects
Particle effects overlay the current screen. Supported effect types:
🔴 The surface decides when the trigger’s moment is, and each surface has only the moments it
can actually hold. An effect whose trigger is not one of its surface’s moments has nothing to fire
in, and nothing is drawn.
🔴
on_flow_complete is not an onboarding moment. The host takes the flow off the screen as
soon as it completes, so an effect armed there would be dismissed in the frame it started. A
celebration at the end of a flow belongs on whatever screen the app shows next.
Fixed in iOS 1.0.84 / Android 1.0.56. Before those versions only
on_appear ever fired: the
overlay re-checked the authored trigger after its parent had already decided to show it, and
refused anything else. A paywall effect left on its Android default — on_purchase — therefore
never ran at all.Blur / Glassmorphism
Blur backdrops use nativeUIVisualEffectView for performant glassmorphism effects:
- Radius — blur intensity
- Tint — overlay color tint
- Saturation — color saturation adjustment
Asset URL Conventions
Each rich-media block carries its own typed URL field. The SDK picks the renderer from the block’stype, not from the file extension, so the field name is what matters:
🔴 A screen SECTION spells the poster
thumbnail_url, without the video_ prefix, because the
screens editor writes it that way. Content blocks and paywall sections use the prefixed name. Both
renderers accept either spelling on either surface, so the prefixed form is always safe.
Embedding Rich Media in Paywalls
Rich media inside a paywall belongs to a section. A section can host an image, a Lottie or Rive animation, or a video, and the SDK draws it when the paywall is presented — there is nothing to forward from your code:- Hero animation — a looping Lottie at the top of the paywall while the user reads the features.
- Feature illustrations — each feature row can carry an SF Symbol, an emoji or a remote image.
- Video proof — a short looping product demo in its own section.
- CTA animation — pulse, glow and bounce are configured on the CTA section itself.
Embedding Rich Media in Onboarding
An onboarding step is an ordered list of content blocks. The rich-media types areimage,
video, lottie and rive; they sit alongside heading, text, button, list, badge,
icon, toggle, divider and spacer in the same list and are laid out in order.
Embedding Rich Media in In-App Messages
A modal, banner or fullscreen message has a hero area at the top, which can host an image (animated GIFs included), a Lottie animation or a short looping video. The body below the hero uses the same content-block system as onboarding. Messages are triggered by event rules in the Console. ImplementAppDNAInAppMessageDelegate to
observe them, or to suppress one:
Animation Playback Control
Playback is authored per block in the Console; the renderer reads the fields off the published document and there is nothing to forward through your own code.
🔴 Video fields are
video_-prefixed and Lottie’s are not. autoplay and loop belong to
Lottie; the video block’s equivalents are video_autoplay and video_loop. And Lottie’s speed is
lottie_speed, not speed — a bare speed is a different field entirely (the particle
background’s slow / normal / fast token), so authoring speed on a Lottie block does nothing.
On iOS the unprefixed autoplay / loop / muted / controls are also decoded for the video
block as a legacy fallback (ContentBlockTypes.swift). Android reads the prefixed form only, so
prefer the prefixed names whenever you inspect or hand-write a document.
Local Asset Embedding
scripts/bundle-assets.ts in the AppDNA repo walks a downloaded config bundle, fetches the images
it references into an appdna-assets/ folder, and rewrites each URL to the relative path
appdna-assets/<hash><ext>. Ship that folder inside your app bundle and BundledImageResolver
loads the file from disk instead of the network, falling through to the remote URL when the file is
not there.
🔴 Images only, and there is no asset:// scheme. The lookup ends in a UIImage, so a rewritten
.json, .lottie, .riv, .mp4 or .mov path resolves to nothing — a config containing those
should not be bundled, because the remote URL that worked before bundling is replaced by a relative
path that does not. Android resolves the same appdna-assets/ paths out of
app/src/main/assets/, with the same images-only limit.
Performance Notes
- Lazy loading — a rich-media block starts loading from its
onAppear, so an asset on a later onboarding step is not fetched while the user is on the first. - Image caching — the SDK installs a 20 MB in-memory / 100 MB on-disk
URLCacheasURLCache.sharedon first use, soAsyncImagereads and the SDK’s own prefetches share it. A host app that installs its own cache afterwards keeps that one. - Memory — players and animations are torn down when the host view disappears, so they do not accumulate across flows.
- Cold start — avoid a large video on the very first onboarding step; use an image there and put video on a later step so playback starts after the config has settled.
- File size — the config a device downloads carries the URL, not the animation, so a large
.lottiecosts a fetch rather than a bigger config. Keep it under a few hundred KB anyway: it is fetched on the step that draws it.
Icons
The SDK supports structured icon references across all modules. Icons are resolved to platform-native equivalents:
Icons are used in onboarding content blocks, paywall feature lists, in-app message CTA buttons, and survey question options.
Content Blocks
Onboarding steps support rich content blocks that are rendered in order. Each block has a type and optional styling:Styling
All visual elements support fine-grained styling through the Console:- Text styles — font family, size, weight, color, alignment, line height, letter spacing, opacity
- Backgrounds — solid color, linear/radial gradient, or remote image with overlay
- Borders — width, color, style, and per-corner radius
- Shadows — offset, blur, spread, and color
- Spacing — per-side padding and margin
- Animations — entry animations (slide, fade, scale), section stagger, CTA pulse/glow/bounce, dismiss animations
The SDK uses platform-native fonts only. Custom web fonts are not supported. Specify system font families like
.systemFont in the Console, and the SDK will render them natively.Dynamic Content Templates
Rich media content supports template interpolation using{{variable}} syntax. Available namespaces:
Use the pipe syntax for fallback values:
{{user.name | there}} renders as “there” if the user name is not set or was identified as null.

