# Hekate Swift

> On-device zero-knowledge proofs on iOS: one async Swift call runs your Rust prover in a detached task, and Task.cancel() reaches Rust.

- URL: https://oumuamua.dev/mobile/swift
- Published: 2026-05-22T15:06:54+07:00
- Updated: 2026-10-12T18:30:17+07:00

Async / cancellation facade over the UniFFI-generated Swift bindings. Maps Swift Concurrency (`Task`, `TaskPriority`,
structured cancellation) onto the synchronous `prove(inputs:cancel:)` C-ABI call that the prover crate exports through
UniFFI.

The wrapper is ~30 lines. Two jobs and nothing more: run the multi-second prove in a detached task, which keeps the
caller's actor free, and route `Task.cancel()` through the FFI `CancelToken` to make a cooperative cancel actually reach Rust.

## Installation

`Package.swift` lives at the repository root. Add the package by repo URL:

```swift Package.swift
dependencies: [
    .package(url: "https://github.com/oumuamua-labs/hekate-mobile.git", from: "<version>"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "Hekate", package: "hekate-mobile"),
        ]
    )
]
```

`<version>` is the release in the line under the title. SwiftPM names a remote package after the last component of its
URL, which makes the product's package `hekate-mobile`.

The package ships only the `HekateProver` and `CancellableToken` protocols and the async extension. `cargo xtask ios-build`
writes `MyProver.xcframework`, `HekateProverCdylib.xcframework` and the Swift bindings file. The app embeds both frameworks
and declares the prover type (`MyProver`) once over the bindings, as the
[package README](https://github.com/oumuamua-labs/hekate-mobile/blob/main/swift/README.md#add-your-prover) shows.

## Usage

```swift
import Hekate

let output = try await MyProver.prove(
    inputs: myInputs,
    priority: .userInitiated
)
```

That's the surface. No cancel tokens, no completion handlers, no `DispatchQueue.global().async { ... }`, no JNI-style
typed-buffer plumbing. The `try` covers every failure mode the Rust prover can produce, the `await` suspends the caller
until the proof is ready or the parent task is cancelled.

`priority` is forwarded to the detached `Task` that runs the synchronous FFI call, which occupies one thread of Swift's
cooperative pool until it returns. `.background` is the right choice for batch or off-screen proving. `.userInitiated`
is acceptable for interactive flows but risks the iOS background-CPU killer if the app suspends mid-prove.

## Cancellation

`Task.cancel()` on the enclosing Swift task automatically calls `CancelToken.request()` through
`withTaskCancellationHandler`. That covers every form of cancellation Swift Concurrency produces:

- parent-driven structured cancellation when a `TaskGroup` aborts,
- view-lifecycle cancellation in SwiftUI when the host view disappears,
- an explicit `cancel()` from a coordinator or `Task` handle.

The Rust prover stops at its next poll of the token, and the `await` throws `ProveError.Cancelled`. There is no manual
cancel-token plumbing on the call site, and there is no `Thread.cancel()` analogue in Swift to misuse.

The cancellation propagation is cooperative on both sides: Rust observes the flag at its next poll and returns. The
witness build takes no token, and a cancel that arrives during it is not seen until the prover runs.

## Panic isolation

The Rust `prove` entry point wraps the prover invocation in `catch_unwind(AssertUnwindSafe(...))`. If the prover panics
for any reason, debug assertion, integer overflow in `debug_assertions`, a bug in the dev's circuit or witness builder,
`catch_unwind` captures it, formats the panic payload, and returns `Err(ProveError::Panic(message))`. UniFFI maps that
variant to a typed Swift `enum ProveError` case:

```swift
do {
    let output = try await MyProver.prove(inputs: myInputs)
} catch ProveError.Panic(let message) {
    // log + surface; the prove call did NOT corrupt your process
} catch ProveError.Cancelled {
    // user-initiated or structured cancel
} catch {
    // every other typed prover error variant
}
```

No `abort()` crosses the FFI boundary. The Rust panic unwinds locally inside `catch_unwind`, the FFI return frame is
intact, and the Swift caller sees a typed error instead of a `SIGABRT` on the next stack frame.

## Next steps

- [Your First ZK Program](https://oumuamua.dev/hekate/docs/getting-started/your-first-zk-program): write the statement in Rust, a private
  payment proved and verified end to end.
- [Preflight](https://oumuamua.dev/hekate/docs/basics/preflight): check a witness against every constraint before the prover runs.
- [Verifier Logic](https://oumuamua.dev/hekate/docs/basics/verifier-logic): verify the proof on a server and pin the program id.
- [Soundness and Security](https://oumuamua.dev/hekate/docs/advanced/soundness-and-security): what a proof guarantees and the checks left
  to you.