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

  1. Define: Create your state machine logic (states, transitions, interactions, inputs) within your .lottie file.

  2. Load Animation: Instantiate a view (DotLottiePlayerView, DotLottiePlayerUIView, or DotLottieAnimation) with your .lottie file.

  3. Load State Machine: Load the desired state machine by its ID.

  4. Start State Machine: Activate the state machine.

  5. Set Inputs: Update input values from your application code to drive transitions.

  6. 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:

MethodReturn TypeDescription
stateMachineLoad(id:)BoolLoads a specific state machine by ID from the .lottie manifest. Returns true on success.
stateMachineLoadData(_:)BoolLoads a state machine from a JSON string for state machines not bundled in the .lottie file.
stateMachineStart(openUrlPolicy:)BoolStarts the loaded state machine. Returns true on success, false if no state machine is loaded.
stateMachineStop()BoolStops the active state machine.
stateMachinePostEvent(_:force:)VoidSends a typed Event to the state machine to trigger pointer-based interactions.
stateMachineFire(event:)BoolFires a named Event input defined in the state machine.
stateMachineSubscribe(observer:)VoidSubscribes a StateMachineObserver to receive state lifecycle callbacks.
stateMachineUnsubscribe(observer:)VoidUnsubscribes 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:

FieldRequiredDescription
initialYesName of the starting state
statesYesArray of state definitions
interactionsNoUser interaction handlers
inputsNoInput variable declarations
{
  "initial": "idle",
  "states": [],
  "interactions": [],
  "inputs": []
}

States

PlaybackState

Controls animation playback. All transitions for a state are defined inside it.

PropertyRequiredDefaultDescription
nameYesUnique state identifier
typeYes"PlaybackState"
animationYesAnimation ID from the a/ directory
autoplayNofalseAuto-start on state entry
loopNofalseEnable continuous looping
loopCountNoinfiniteNumber of loop iterations
modeNo"Forward""Forward" | "Reverse" | "Bounce" | "ReverseBounce"
speedNo1.0Playback speed multiplier
segmentNoNamed segment or frame range
backgroundColorNoBackground color as hex number (e.g. 0xFFFFFF)
finalNofalsePrevents further transitions when true
entryActionsNo[]Actions executed on state entry
exitActionsNo[]Actions executed on state exit
transitionsYesArray of transition rules

GlobalState

Overrides behaviors across all states. GlobalState transitions are checked before regular state transitions, giving them higher precedence.

PropertyRequiredDescription
nameYesUnique state identifier
typeYes"GlobalState"
entryActionsNoOverride entry actions
exitActionsNoOverride exit actions
transitionsYesOverride 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.

PropertyRequiredDescription
typeYes"Transition"
toStateYesTarget state name
guardsNoConditions (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.

PropertyRequiredDescription
typeYes"Tweened"
toStateYesTarget state name
durationYesDuration in seconds
easingYesCubic Bézier curve [x1, y1, x2, y2]
guardsNoConditions
{
  "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

PropertyRequiredDescription
typeYes"Numeric"
inputNameYesNumeric input to check
conditionTypeYes"Equal" | "NotEqual" | "GreaterThan" | "GreaterThanOrEqual" | "LessThan" | "LessThanOrEqual"
compareToYesNumber or "$inputName" reference

String Guard

Case-sensitive comparison.

PropertyRequiredDescription
typeYes"String"
inputNameYesString input to check
conditionTypeYes"Equal" | "NotEqual"
compareToYesString value or "$inputName" reference

Boolean Guard

PropertyRequiredDescription
typeYes"Boolean"
inputNameYesBoolean input to check
conditionTypeYes"Equal" | "NotEqual"
compareToYestrue / 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.

PropertyRequiredDescription
typeYes"Event"
inputNameYesEvent 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 TypePropertiesDescription
Numericname, value (Number)Stores a numeric value
Stringname, value (String)Stores a string value
Booleanname, value (Boolean)Stores a boolean value
Eventname (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:

MethodDescription
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.

ActionKey PropertiesDescription
SetThemevalue (String)Applies a theme by ID
OpenUrlurl, targetOpens an external URL
IncrementinputName, value (default 1)Increments a numeric input
DecrementinputName, value (default 1)Decrements a numeric input
ToggleinputNameInverts a boolean input
SetBooleaninputName, valueSets a boolean input
SetStringinputName, valueSets a string input
SetNumericinputName, valueSets a numeric input
FireinputNameFires an event input
ResetinputNameResets an input to its initial value
SetFramevalue (Number)Sets the current animation frame
SetProgressvalue (0–1 normalized)Sets animation progress
FireCustomEventvalue (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

TypeDescription
ClickUser click or tap
PointerDownPointer pressed
PointerUpPointer released
PointerEnterPointer enters the animation area or layer
PointerExitPointer leaves the animation area or layer
PointerMovePointer moves

All pointer interactions share the same shape:

PropertyRequiredDescription
typeYesInteraction type (see above)
layerNameNoRestricts the trigger to a specific layer (case-sensitive, exact match)
actionsYesActions 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

  1. State machine not loading/starting:

    • Verify the id passed to stateMachineLoad matches the ID in the .lottie manifest.

    • Check the boolean return values of stateMachineLoad and stateMachineStart.

    • Ensure the animation itself loaded successfully before calling state machine methods.

  2. Transitions not firing:

    • Check that all guard conditions are met (guards are ANDed within a transition).

    • For Event guards, confirm the event was fired via stateMachineFire(event:).

    • Confirm stateMachineStart() returned true.

  3. Animation playback issues in states:

    • Ensure segment names or frame numbers in PlaybackState exist in the animation JSON.

  4. Use StateMachineObserver for logging: Implement the protocol and subscribe with stateMachineSubscribe to 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 stateMachineLoad and stateMachineStart return values.

  • Use Event inputs over pointer events when your interaction logic doesn't depend on pointer coordinates — they're simpler and more portable.

  • Cleanup: Call stateMachineStop() and stateMachineUnsubscribe() in onDisappear or deinit.

  • GlobalState for overlays: Use GlobalState for transitions that should be available regardless of the current state (e.g., an error overlay or dismiss interaction).

Last updated: June 9, 2026 at 8:42 AMEdit this page