# 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:

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

```swift
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 |

```json
{
  "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 |

```json
{
  "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. |

```json
{
  "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                            |

```json
{
  "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 |

```json
"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

```swift
// 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.

```json
"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                                                      |

```json
{
  "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).

```json
{
  "type": "OnComplete",
  "stateName": "active",
  "actions": [{ "type": "Fire", "inputName": "animDone" }]
}
```

### OnLoopComplete

Fires after every loop iteration.

```json
{
  "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:

```swift
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
}
```

```swift
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.

```swift
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).

## Related Topics

- [dotLottie State Machine Specification](https://dotlottie.io/spec/2.0/#state-machines)
- [iOS API Reference](/en/runtimes/distributions/ios/v0.x/api-reference)
- [iOS Usage Guide](/en/runtimes/distributions/ios/v0.x/usage-guide)
