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

> Integrate GoRules into your iOS application.

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

## Installation

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

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 = 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

The loader pattern enables dynamic decision loading from any storage backend. Implement the `ZenDecisionLoaderCallback` protocol with a `func load(key: String) async throws -> Data?` method. Use a thread-safe cache for optimal performance.

### Bundle

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

class BundleLoader: ZenDecisionLoaderCallback {
    private var cache: [String: Data] = [:]
    private let lock = NSLock()

    func load(key: String) async throws -> Data? {
        lock.lock()
        if let cached = cache[key] {
            lock.unlock()
            return cached
        }
        lock.unlock()

        guard let url = Bundle.main.url(forResource: key, withExtension: nil),
              let data = try? Data(contentsOf: url) else {
            return nil
        }

        lock.lock()
        cache[key] = data
        lock.unlock()

        return data
    }
}

func createEngine() -> ZenEngine {
    let loader = BundleLoader()
    return ZenEngine(loader: loader, customNode: nil)
}
```

### Documents directory

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

class DocumentsLoader: ZenDecisionLoaderCallback {
    private var cache: [String: Data] = [:]
    private let lock = NSLock()
    private let documentsURL: URL

    init() {
        documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    }

    func load(key: String) async throws -> Data? {
        lock.lock()
        if let cached = cache[key] {
            lock.unlock()
            return cached
        }
        lock.unlock()

        let fileURL = documentsURL.appendingPathComponent("rules/\(key)")
        let data = try Data(contentsOf: fileURL)

        lock.lock()
        cache[key] = data
        lock.unlock()

        return data
    }
}

func createEngine() -> ZenEngine {
    let loader = DocumentsLoader()
    return ZenEngine(loader: loader, customNode: nil)
}
```

### Firebase Remote Config

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

class RemoteConfigLoader: ZenDecisionLoaderCallback {
    private let remoteConfig = RemoteConfig.remoteConfig()
    private var cache: [String: Data] = [:]
    private let lock = NSLock()

    func load(key: String) async throws -> Data? {
        lock.lock()
        if let cached = cache[key] {
            lock.unlock()
            return cached
        }
        lock.unlock()

        let configKey = "decision_\(key.replacingOccurrences(of: ".", with: "_"))"
        let value = remoteConfig.configValue(forKey: configKey).dataValue

        lock.lock()
        cache[key] = value
        lock.unlock()

        return value
    }
}

func createEngine() -> ZenEngine {
    let loader = RemoteConfigLoader()
    return ZenEngine(loader: loader, customNode: nil)
}
```

### Remote URL with caching

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

class RemoteLoader: ZenDecisionLoaderCallback {
    private var cache: [String: Data] = [:]
    private let lock = NSLock()
    private let cacheURL: URL

    init() {
        cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
            .appendingPathComponent("rules")
    }

    func load(key: String) async throws -> Data? {
        lock.lock()
        if let cached = cache[key] {
            lock.unlock()
            return cached
        }
        lock.unlock()

        let fileURL = cacheURL.appendingPathComponent(key)

        let data: Data
        if FileManager.default.fileExists(atPath: fileURL.path) {
            data = try Data(contentsOf: fileURL)
        } else {
            let url = URL(string: "https://api.example.com/rules/\(key)")!
            let (responseData, _) = try await URLSession.shared.data(from: url)
            try FileManager.default.createDirectory(at: cacheURL, withIntermediateDirectories: true)
            try responseData.write(to: fileURL)
            data = responseData
        }

        lock.lock()
        cache[key] = data
        lock.unlock()

        return data
    }
}

func createEngine() -> ZenEngine {
    let loader = RemoteLoader()
    return ZenEngine(loader: loader, customNode: nil)
}
```

### Remote URL with zip

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

var rules: [String: Data] = [:]

func initializeRules() async throws {
    let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let cacheFile = cacheURL.appendingPathComponent("decisions.zip")

    let zipData: Data
    if FileManager.default.fileExists(atPath: cacheFile.path) {
        zipData = try Data(contentsOf: cacheFile)
    } else {
        let url = URL(string: "https://api.example.com/decisions.zip")!
        let (responseData, _) = try await URLSession.shared.data(from: url)
        try responseData.write(to: cacheFile)
        zipData = responseData
    }

    // Extract all decisions from zip
    guard let archive = Archive(data: zipData, accessMode: .read) else { return }
    for entry in archive {
        var entryData = Data()
        _ = try archive.extract(entry) { chunk in
            entryData.append(chunk)
        }
        rules[entry.path] = entryData
    }
}

class ZipLoader: ZenDecisionLoaderCallback {
    func load(key: String) async throws -> Data? {
        return rules[key]
    }
}

func createEngine() -> ZenEngine {
    let loader = ZipLoader()
    return ZenEngine(loader: loader, customNode: nil)
}
```

### Firebase Storage with zip

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

var rules: [String: Data] = [:]

func initializeRules() async throws {
    let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let cacheFile = cacheURL.appendingPathComponent("decisions.zip")

    let zipData: Data
    if FileManager.default.fileExists(atPath: cacheFile.path) {
        zipData = try Data(contentsOf: cacheFile)
    } else {
        // Download from Firebase Storage
        let storage = Storage.storage()
        let ref = storage.reference().child("decisions.zip")
        let maxSize: Int64 = 10 * 1024 * 1024 // 10MB max
        zipData = try await ref.data(maxSize: maxSize)
        try zipData.write(to: cacheFile)
    }

    // Extract all decisions from zip
    guard let archive = Archive(data: zipData, accessMode: .read) else { return }
    for entry in archive {
        var entryData = Data()
        _ = try archive.extract(entry) { chunk in
            entryData.append(chunk)
        }
        rules[entry.path] = entryData
    }
}

class FirebaseStorageLoader: ZenDecisionLoaderCallback {
    func load(key: String) async throws -> Data? {
        return rules[key]
    }
}

func createEngine() -> ZenEngine {
    let loader = FirebaseStorageLoader()
    return ZenEngine(loader: loader, customNode: nil)
}
```

## Async/Await

Evaluation functions are async, integrating natively with Swift concurrency:

```swift theme={null}
import ZenUniffi

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

        var results: [ZenEvaluateResponse] = []
        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(trace: true, maxDepth: nil)

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

if let traceData = response.trace,
   let traceString = String(data: traceData, encoding: .utf8) {
    print("Trace: \(traceString)")
}
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 = 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.
