Understanding State Machines
Explains how state machines work in the dotLottie iOS Player. Covers the core model, workflow, API, event posting, and debugging.
Understanding State Machines
This page explains how state machines work in the iOS player — what they are, how they integrate with animation playback, and when to use them. It also documents the API for loading, starting, and interacting with state machines.
Note: State machine support is an actively developing feature. APIs and capabilities might change.
Core Workflow
Define: Create your state machine logic (states, transitions, interactions, inputs) within your
.lottiefile.Load Animation: Instantiate a view (
DotLottiePlayerView,DotLottiePlayerUIView, orDotLottieAnimation) with your.lottiefile.Load State Machine: Load the desired state machine by its ID.
Start State Machine: Activate the state machine.
Set Inputs: Update input values from your application code to drive transitions.
Stop (Optional): Stop the state machine when interactivity is no longer needed.
iOS Player API for State Machines
You control state machines using methods on the DotLottieAnimation instance:
| Method | Return Type | Description |
stateMachineLoad(id:) | Bool | Loads a specific state machine by ID from the .lottie manifest. Returns true on success. |
stateMachineLoadData(_:) | Bool | Loads a state machine from a JSON string for state machines not bundled in the .lottie file. |
stateMachineStart(openUrlPolicy:) | Bool | Starts the loaded state machine. Returns true on success, false if no state machine is loaded. |
stateMachineStop() | Bool | Stops the active state machine. |
stateMachinePostEvent(_:force:) | Void | Sends a typed Event to the state machine to trigger pointer-based interactions. |
stateMachineFire(event:) | Bool | Fires a named Event input defined in the state machine. |
stateMachineSubscribe(observer:) | Void | Subscribes a StateMachineObserver to receive state lifecycle callbacks. |
stateMachineUnsubscribe(observer:) | Void | Unsubscribes a StateMachineObserver. |
Important: Ensure the main animation is loaded before attempting to load and start the state machine. Check the boolean return values of stateMachineLoad and stateMachineStart.
import DotLottie
import SwiftUI
struct StateMachineView: View {
@StateObject var dotLottieViewModel = DotLottieAnimation(fileName: "interactive_animation")
class MyStateMachineObserver: StateMachineObserver {
func onTransition(previousState: String, newState: String) {
print("Transition: \(previousState) -> \(newState)")
}
func onStateEntered(enteringState: String) {
print("Entered State: \(enteringState)")
}
func onStateExit(leavingState: String) {
print("Exited State: \(leavingState)")
}
}
let observer = MyStateMachineObserver()
var body: some View {
VStack {
dotLottieViewModel.view()
.frame(height: 300)
.onAppear {
if dotLottieViewModel.stateMachineLoad(id: "myButtonFSM") {
dotLottieViewModel.stateMachineSubscribe(observer: observer)
dotLottieViewModel.stateMachineStart()
}
}
.onDisappear {
dotLottieViewModel.stateMachineStop()
dotLottieViewModel.stateMachineUnsubscribe(observer: observer)
}
Button("Trigger Event") {
dotLottieViewModel.stateMachineFire(event: "buttonClicked")
}
.padding()
}
}
}State Machine Structure
State machines are stored as JSON files in the s/ directory of a .lottie archive and registered in manifest.json. A state machine has four top-level fields:
| Field | Required | Description |
initial | Yes | Name of the starting state |
states | Yes | Array of state definitions |
interactions | No | User interaction handlers |
inputs | No | Input variable declarations |
{
"initial": "idle",
"states": [],
"interactions": [],
"inputs": []
}States
PlaybackState
Controls animation playback. All transitions for a state are defined inside it.
| Property | Required | Default | Description |
name | Yes | — | Unique state identifier |
type | Yes | — | "PlaybackState" |
animation | Yes | — | Animation ID from the a/ directory |
autoplay | No | false | Auto-start on state entry |
loop | No | false | Enable continuous looping |
loopCount | No | infinite | Number of loop iterations |
mode | No | "Forward" | "Forward" | "Reverse" | "Bounce" | "ReverseBounce" |
speed | No | 1.0 | Playback speed multiplier |
segment | No | — | Named segment or frame range |
backgroundColor | No | — | Background color as hex number (e.g. 0xFFFFFF) |
final | No | false | Prevents further transitions when true |
entryActions | No | [] | Actions executed on state entry |
exitActions | No | [] | Actions executed on state exit |
transitions | Yes | — | Array of transition rules |
GlobalState
Overrides behaviors across all states. GlobalState transitions are checked before regular state transitions, giving them higher precedence.
| Property | Required | Description |
name | Yes | Unique state identifier |
type | Yes | "GlobalState" |
entryActions | No | Override entry actions |
exitActions | No | Override exit actions |
transitions | Yes | Override transition rules |
{
"name": "globalOverlay",
"type": "GlobalState",
"transitions": [
{
"type": "Transition",
"toState": "idle",
"guards": [{ "type": "Event", "inputName": "dismiss" }]
}
]
}Transitions
Transitions are declared inside each state's transitions array. They are evaluated in declaration order — the first passing transition fires.
Immediate Transition
Changes state instantly when all guards pass.
| Property | Required | Description |
type | Yes | "Transition" |
toState | Yes | Target state name |
guards | No | Conditions (all must pass — ANDed). A transition with no guards is a fallthrough. |
{
"type": "Transition",
"toState": "active",
"guards": [{ "type": "Boolean", "inputName": "isActive", "conditionType": "Equal", "compareTo": true }]
}Tweened Transition
Animates the state change over a specified duration. Input modifications are blocked while a tween is in progress; the state change applies only after the tween completes.
| Property | Required | Description |
type | Yes | "Tweened" |
toState | Yes | Target state name |
duration | Yes | Duration in seconds |
easing | Yes | Cubic Bézier curve [x1, y1, x2, y2] |
guards | No | Conditions |
{
"type": "Tweened",
"toState": "loading",
"duration": 0.5,
"easing": [0.25, 0.1, 0.25, 1],
"guards": [{ "type": "Event", "inputName": "startLoading" }]
}Guards
Guards are conditions on transitions. All guards within a single transition are ANDed — all must be true for the transition to fire.
Numeric Guard
| Property | Required | Description |
type | Yes | "Numeric" |
inputName | Yes | Numeric input to check |
conditionType | Yes | "Equal" | "NotEqual" | "GreaterThan" | "GreaterThanOrEqual" | "LessThan" | "LessThanOrEqual" |
compareTo | Yes | Number or "$inputName" reference |
String Guard
Case-sensitive comparison.
| Property | Required | Description |
type | Yes | "String" |
inputName | Yes | String input to check |
conditionType | Yes | "Equal" | "NotEqual" |
compareTo | Yes | String value or "$inputName" reference |
Boolean Guard
| Property | Required | Description |
type | Yes | "Boolean" |
inputName | Yes | Boolean input to check |
conditionType | Yes | "Equal" | "NotEqual" |
compareTo | Yes | true / false or "$inputName" reference |
Event Guard
Edge-triggered. The event is stored in a single slot — new events overwrite the previous, and the event is consumed after a successful transition.
| Property | Required | Description |
type | Yes | "Event" |
inputName | Yes | Event input name to check |
Inputs
Inputs are variables declared at the root of the state machine. They are readable and writable by guards and actions, and settable at runtime via the iOS API.
| Input Type | Properties | Description |
Numeric | name, value (Number) | Stores a numeric value |
String | name, value (String) | Stores a string value |
Boolean | name, value (Boolean) | Stores a boolean value |
Event | name (no value) | A triggerable event slot |
"inputs": [
{ "type": "Numeric", "name": "score", "value": 0 },
{ "type": "String", "name": "userRole", "value": "guest" },
{ "type": "Boolean", "name": "isActive", "value": false },
{ "type": "Event", "name": "buttonClicked" }
]Setting Inputs from iOS
// Set input values
dotLottieViewModel.stateMachineSetBooleanInput(key: "isActive", value: true)
dotLottieViewModel.stateMachineSetNumericInput(key: "score", value: 100.5)
dotLottieViewModel.stateMachineSetStringInput(key: "userRole", value: "admin")
// Fire an Event input
dotLottieViewModel.stateMachineFire(event: "buttonClicked")
// Get input values
if let isActive = dotLottieViewModel.stateMachineGetBooleanInput(key: "isActive") {
print("isActive: \(isActive)")
}
if let score = dotLottieViewModel.stateMachineGetNumericInput(key: "score") {
print("score: \(score)")
}
// Get all inputs
let allInputs = dotLottieViewModel.stateMachineGetInputs()Input API methods:
| Method | Description |
stateMachineSetBooleanInput(key:value:) | Sets a boolean input |
stateMachineSetNumericInput(key:value:) | Sets a numeric input |
stateMachineSetStringInput(key:value:) | Sets a string input |
stateMachineGetBooleanInput(key:) -> Bool? | Retrieves a boolean input |
stateMachineGetNumericInput(key:) -> Float? | Retrieves a numeric input |
stateMachineGetStringInput(key:) -> String? | Retrieves a string input |
stateMachineGetInputs() -> [String: String] | Returns all inputs and current values |
Actions
Actions are operations executed during state entry/exit or in response to interactions. They are synchronous.
| Action | Key Properties | Description |
SetTheme | value (String) | Applies a theme by ID |
OpenUrl | url, target | Opens an external URL |
Increment | inputName, value (default 1) | Increments a numeric input |
Decrement | inputName, value (default 1) | Decrements a numeric input |
Toggle | inputName | Inverts a boolean input |
SetBoolean | inputName, value | Sets a boolean input |
SetString | inputName, value | Sets a string input |
SetNumeric | inputName, value | Sets a numeric input |
Fire | inputName | Fires an event input |
Reset | inputName | Resets an input to its initial value |
SetFrame | value (Number) | Sets the current animation frame |
SetProgress | value (0–1 normalized) | Sets animation progress |
FireCustomEvent | value (String) | Fires a custom event to the host application |
Action values can reference other inputs using $inputName syntax.
"entryActions": [
{ "type": "SetTheme", "value": "dark_theme" },
{ "type": "Increment", "inputName": "visitCount" },
{ "type": "FireCustomEvent", "value": "stateActivated" }
]Interactions
Interactions define how user input maps to state machine actions. They are declared in the top-level interactions array and execute their actions array when triggered.
Pointer Interactions
| Type | Description |
Click | User click or tap |
PointerDown | Pointer pressed |
PointerUp | Pointer released |
PointerEnter | Pointer enters the animation area or layer |
PointerExit | Pointer leaves the animation area or layer |
PointerMove | Pointer moves |
All pointer interactions share the same shape:
| Property | Required | Description |
type | Yes | Interaction type (see above) |
layerName | No | Restricts the trigger to a specific layer (case-sensitive, exact match) |
actions | Yes | Actions to execute |
{
"type": "Click",
"layerName": "button_layer",
"actions": [{ "type": "Toggle", "inputName": "isActive" }]
}OnComplete
Fires when animation playback reaches its final end (after a single iteration when loop: false, or after the last iteration when loopCount is set).
{
"type": "OnComplete",
"stateName": "active",
"actions": [{ "type": "Fire", "inputName": "animDone" }]
}OnLoopComplete
Fires after every loop iteration.
{
"type": "OnLoopComplete",
"stateName": "loading",
"actions": [{ "type": "Increment", "inputName": "loopCount" }]
}Programmatic Pointer Events (iOS API)
Pointer and click interactions defined in the state machine's interactions array (e.g., Click, PointerDown, PointerEnter) are handled automatically by the iOS player based on user touches on the animation view. You do not need to post these events manually for normal interactions.
The stateMachinePostEvent method is available for cases where you need to synthesize pointer events programmatically — for example, triggering an interaction from custom gesture logic outside the animation view:
public enum Event {
case pointerDown(x: Float, y: Float)
case pointerUp(x: Float, y: Float)
case pointerMove(x: Float, y: Float)
case pointerEnter(x: Float, y: Float)
case pointerExit(x: Float, y: Float)
case click(x: Float, y: Float)
case onComplete
case onLoopComplete
}dotLottieViewModel.stateMachinePostEvent(Event.click(x: 150, y: 150))
dotLottieViewModel.stateMachinePostEvent(Event.pointerDown(x: 10, y: 20))Debugging and Troubleshooting
State machine not loading/starting:
Verify the
idpassed tostateMachineLoadmatches the ID in the.lottiemanifest.Check the boolean return values of
stateMachineLoadandstateMachineStart.Ensure the animation itself loaded successfully before calling state machine methods.
Transitions not firing:
Check that all guard conditions are met (guards are ANDed within a transition).
For
Eventguards, confirm the event was fired viastateMachineFire(event:).Confirm
stateMachineStart()returnedtrue.
Animation playback issues in states:
Ensure
segmentnames or frame numbers inPlaybackStateexist in the animation JSON.
Use
StateMachineObserverfor logging: Implement the protocol and subscribe withstateMachineSubscribeto log state entries, exits, and transitions.
class MyObserver: StateMachineObserver {
func onTransition(previousState: String, newState: String) {
print("Transition: \(previousState) -> \(newState)")
}
func onStateEntered(enteringState: String) {
print("Entered: \(enteringState)")
}
func onStateExit(leavingState: String) {
print("Exited: \(leavingState)")
}
}Best Practices
Check return values: Always check
stateMachineLoadandstateMachineStartreturn values.Use
Eventinputs over pointer events when your interaction logic doesn't depend on pointer coordinates — they're simpler and more portable.Cleanup: Call
stateMachineStop()andstateMachineUnsubscribe()inonDisappearordeinit.GlobalState for overlays: Use
GlobalStatefor transitions that should be available regardless of the current state (e.g., an error overlay or dismiss interaction).