> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS Rules Engine

> Install the ZEN Engine Swift package and evaluate decisions in your iOS app.

Install the ZEN Engine and evaluate your first decision in iOS.

## Installation

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/gorules/zen-ios", from: "2.0.0")
]
```

Or in Xcode: File → Add Package Dependencies → Enter the repository URL.

## Basic usage

```swift theme={null}
import UIKit
import ZenUniffi

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            guard let ruleData = Bundle.main.url(forResource: "pricing", withExtension: "json")
                .flatMap({ try? Data(contentsOf: $0) }) else {
                return
            }

            let engine = try ZenEngine(loader: nil, customNode: nil)
            let decision = try engine.createDecision(content: ruleData)

            let input = """
                {
                    "customer": { "tier": "gold", "yearsActive": 3 },
                    "order": { "subtotal": 150, "items": 5 }
                }
            """.data(using: .utf8)!

            let response = try await decision.evaluate(context: input, options: nil)

            if let resultString = String(data: response.result, encoding: .utf8) {
                print(resultString)
                // => {"discount":0.15,"freeShipping":true}
            }
        }
    }
}
```

## Loader

`ZenEngine` accepts an optional `ZenLoader` that serves decisions by key. Use the `static`, `filesystem`, or `zip` variants for common backends, or `callback` for custom loading logic.

### Static

Register decisions in memory. Use this for rules shipped in your app bundle:

```swift theme={null}
import ZenUniffi
import Foundation

func createEngine() throws -> ZenEngine {
    let url = Bundle.main.url(forResource: "pricing", withExtension: "json")!
    let pricing = try Data(contentsOf: url)

    let loader = ZenLoader.static(content: ["pricing.json": pricing])
    return try ZenEngine(loader: loader, customNode: nil)
}
```

### File system

Load decisions from files under a root directory, such as the Documents directory. Keys resolve to paths relative to the root:

```swift theme={null}
import ZenUniffi
import Foundation

func createEngine() throws -> ZenEngine {
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let rulesURL = documentsURL.appendingPathComponent("rules")

    let loader = ZenLoader.filesystem(path: rulesURL.path)
    return try ZenEngine(loader: loader, customNode: nil)
}
```

### Zip archive

Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release once and hand the bytes to the engine:

```swift theme={null}
import ZenUniffi
import Foundation

func createEngine() async throws -> ZenEngine {
    let url = URL(string: "https://api.example.com/decisions.zip")!
    let (zipData, _) = try await URLSession.shared.data(from: url)

    let loader = ZenLoader.zip(bytes: zipData)
    return try ZenEngine(loader: loader, customNode: nil)
}
```

### Custom loader

For any other backend, implement `ZenDecisionLoaderCallback` and wrap it in the `callback` variant:

```swift theme={null}
import ZenUniffi
import Foundation

class BundleLoader: ZenDecisionLoaderCallback {
    func load(key: String) async throws -> Data? {
        guard let url = Bundle.main.url(forResource: key, withExtension: nil) else {
            return nil
        }
        return try Data(contentsOf: url)
    }
}

func createEngine() throws -> ZenEngine {
    let loader = ZenLoader.callback(callback: BundleLoader())
    return try ZenEngine(loader: loader, customNode: nil)
}
```

Returning `nil` from the callback reports the key as not found.

## Batch evaluation

Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch:

```swift theme={null}
let results = await engine.evaluateBatch(
    requests: [
        ZenBatchRequest(key: "pricing.json", context: #"{ "amount": 100 }"#.data(using: .utf8)!),
        ZenBatchRequest(key: "pricing.json", context: #"{ "amount": 250 }"#.data(using: .utf8)!)
    ],
    options: nil
)

for result in results {
    if result.success, let data = result.data,
       let resultString = String(data: data.result, encoding: .utf8) {
        print(resultString)
    } else {
        print("Evaluation failed: \(result.error ?? "unknown")")
    }
}
```

## Async/Await

Evaluation functions are async, integrating natively with Swift concurrency:

```swift theme={null}
import ZenUniffi

func evaluateMultiple(decision: ZenDecision, inputs: [Data]) async throws -> [ZenEngineResponse] {
    try await withThrowingTaskGroup(of: ZenEngineResponse.self) { group in
        for input in inputs {
            group.addTask {
                try await decision.evaluate(context: input, options: nil)
            }
        }

        var results: [ZenEngineResponse] = []
        for try await result in group {
            results.append(result)
        }
        return results
    }
}
```

## Error handling

```swift theme={null}
import ZenUniffi

do {
    let response = try await decision.evaluate(context: input, options: nil)
    if let resultString = String(data: response.result, encoding: .utf8) {
        print(resultString)
    }
} catch let error as ZenError {
    print("Evaluation failed: \(error.localizedDescription)")
} catch {
    print("Unexpected error: \(error)")
}
```

## Tracing

Enable tracing to inspect decision execution:

```swift theme={null}
import ZenUniffi

let options = ZenEvaluateOptions(maxDepth: nil, trace: true)

let response = try await decision.evaluate(context: input, options: options)

if let trace = response.trace {
    for (nodeId, nodeTrace) in trace {
        print("[\(nodeTrace.order)] \(nodeTrace.name): \(String(data: nodeTrace.output, encoding: .utf8) ?? "")")
    }
}
print("Performance: \(response.performance)")
```

## Expression utilities

Evaluate ZEN expressions outside of a decision context:

```swift theme={null}
import ZenUniffi

// Standard expressions
let result = try evaluateExpression(
    expression: "a + b",
    context: #"{ "a": 5, "b": 3 }"#.data(using: .utf8)!
)
// => 8

let total = try evaluateExpression(
    expression: "sum(items)",
    context: #"{ "items": [1, 2, 3, 4] }"#.data(using: .utf8)!
)
// => 10

// Unary expressions (comparison against $)
let isValid = try evaluateUnaryExpression(
    expression: ">= 5",
    context: #"{ "$": 10 }"#.data(using: .utf8)!
)
// => true

let inList = try evaluateUnaryExpression(
    expression: "'US', 'CA', 'MX'",
    context: #"{ "$": "US" }"#.data(using: .utf8)!
)
// => true
```

## Best practices

**Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations.

```swift theme={null}
class RulesEngine {
    static let shared = RulesEngine()
    private let engine: ZenEngine

    private init() {
        engine = try! ZenEngine(loader: nil, customNode: nil)
    }

    func evaluate(decision: Data, context: Data) async throws -> Data {
        let dec = try engine.createDecision(content: decision)
        let response = try await dec.evaluate(context: context, options: nil)
        return response.result
    }
}
```

**Cache decisions persistently.** Use the Caches or Documents directory to cache downloaded decisions for offline use.

**Evaluate off the main thread.** Use `Task` or `Task.detached` to avoid blocking the main thread during evaluation.

**Bundle fallback decisions.** Include decisions in your app bundle as fallback for first launch or network failures.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.