# iOS Workflow

## Prerequisites

| Tool | Install |
|------|---------|
| Xcode 26+ | Mac App Store |
| xcodegen | `brew install xcodegen` |
| Rust targets | `rustup target add aarch64-apple-ios aarch64-apple-ios-sim` |
| libimobiledevice | `brew install libimobiledevice` |
| Submodules | `git submodule update --init --recursive` |

## Commands

### Build + Run

```bash
./scripts/run-ios.sh device              # full: Rust → xcframework → Xcode → install → launch
./scripts/run-ios.sh sim                 # simulator
./scripts/run-ios.sh device --release    # release build
./scripts/run-ios.sh device --no-build   # relaunch without rebuilding
./scripts/run-ios.sh device --select-device  # pick device
./scripts/run-ios.sh sim --no-build --launch-url 'zedra://...'  # deep link
```

### Native-Only Rebuild (~5s vs ~60s)

Xcode builds run the Rust build target by default. Skip it when only
changing Swift/ObjC:

```bash
(cd ios && xcodegen generate && pod install)
ZEDRA_SKIP_RUST_XCODE_BUILD=1 xcodebuild build \
  -workspace ios/Zedra.xcworkspace -scheme Zedra \
  -destination "id=<DEVICE_ID>" -allowProvisioningUpdates -quiet
```

### Logs

```bash
./scripts/log-ios.sh                     # stream all logs via USB
./scripts/log-ios.sh --filter zedra      # filtered
./scripts/log-ios.sh --select-device     # choose device
./scripts/log-ios.sh --simulator         # stream from a booted simulator
```

For physical devices, the log script only shows USB-paired devices visible to
`libimobiledevice`. Use `--select-device` after switching devices.

## Build Pipeline

`./scripts/run-ios.sh`:

```
crates/zedra/ (Rust)
  → ./scripts/build-ios.sh --device or --sim
  → cargo build --target aarch64-apple-ios or aarch64-apple-ios-sim -p zedra
  → verify target/.../libzedra.a exists
  → xcodebuild -create-xcframework
  → ios/ZedraFFI.xcframework/
  → xcodegen generate when ios/project.yml changed
  → pod install when CocoaPods state is stale
  → xcodebuild build with ZEDRA_SKIP_RUST_XCODE_BUILD=1
  → xcrun devicectl or simctl install + launch
```

Xcode builds from the IDE:

```
Zedra app target
  → depends on ZedraRustFFI aggregate target
  → scripts/xcode-build-rust.sh reads PLATFORM_NAME / SDK_NAME / CONFIGURATION
  → ./scripts/build-ios.sh --device or --sim [--release]
  → Xcode links ios/ZedraFFI.xcframework
```

`run-ios.sh` uses `ios/Zedra.xcworkspace` when CocoaPods has created it, and
falls back to `ios/Zedra.xcodeproj` otherwise. Direct `xcodebuild` commands for
Firebase/CocoaPods builds should use the workspace.

Debug builds do not require `ios/Zedra/GoogleService-Info.plist`; the Firebase
bridge disables itself when the bundled config is absent. Release builds require
the plist and the Xcode build fails with an explicit error if it is missing.

**Note**: `Cargo.toml` declares `crate-type = ["cdylib", "staticlib"]`. The
cdylib is for Android. On iOS, `build-ios.sh` builds the `zedra` package for
the selected target, fails on cargo errors, and checks for the `.a` before
packaging the XCFramework. Weak stubs in `crates/zedra/src/ios_stub.c` satisfy
Rust-to-iOS symbols during cargo linking; the real Swift/Obj-C definitions are
provided by Xcode sources.

## Project Structure

```
scripts/
  build-ios.sh        # Rust → ZedraFFI.xcframework
  xcode-build-rust.sh # Xcode target wrapper around build-ios.sh
  run-ios.sh          # Full pipeline: build + install + launch
  log-ios.sh          # Stream device logs

crates/zedra/src/ios/bridge.rs  # Rust side of iOS FFI
crates/zedra/src/ios_stub.c     # Weak C stubs for Rust → Swift symbols

ios/
  project.yml         # XcodeGen spec (source of truth)
  Zedra.xcodeproj/    # Generated — do not edit
  ZedraFFI.xcframework/  # .a files gitignored, rebuilt by build-ios.sh
  Zedra/
    main.m                     # UIApplication bootstrap
    ZedraAppDelegate.swift     # App delegate
    GPUIRuntimeController.swift  # GPUI lifecycle, frame loop, keyboard
    NativeBridge.swift         # Swift→C bridge (alerts, URLs, Firebase)
    KeyboardSupporter.swift    # Keyboard accessory view
    QRScanner.swift            # AVFoundation QR scanner

include/
  zedra_ios.h         # Auto-generated by cbindgen (do not edit)
  gpui_ios.h          # GPUI iOS FFI header (from vendor/zed)
```

## Architecture

### Startup Sequence

```
main.m → UIApplicationMain → ZedraAppDelegate
  → GPUIRuntimeController.launch()
    → gpui_ios_initialize()        # GPUI state
    → zedra_launch_gpui()          # GPUI App + IosPlatform + IosBridge
    → gpui_ios_did_finish_launching()  # open window, create Metal layer
    → CADisplayLink @ 60 FPS       # drives gpui_ios_request_frame()
```

### FFI Boundaries

**Swift → Rust** (two headers):
- `gpui_ios.h`: lifecycle (`gpui_ios_initialize`, `gpui_ios_request_frame`, keyboard show/hide)
- `zedra_ios.h`: app-specific (`zedra_launch_gpui`, `zedra_deeplink_received`, `zedra_qr_scanner_result`)

**Rust → Swift** (C symbols via `@_cdecl` in NativeBridge.swift):
- `ios_present_alert`, `ios_present_selection`, `ios_open_url`
- `ios_present_qr_scanner`, `zedra_log_event`

### Adding FFI

**Rust → Swift**:
1. Implement in Swift with `@_cdecl` (usually `NativeBridge.swift`)
2. Declare `unsafe extern "C" { fn my_func(...); }` in `crates/zedra/src/ios/bridge.rs`
3. Add weak stub in `crates/zedra/src/ios_stub.c`

**Swift → Rust**:
1. Add `#[unsafe(no_mangle)] pub extern "C" fn` in `crates/zedra/src/ios/bridge.rs`
2. Run `./scripts/build-ios.sh` (cbindgen regenerates `zedra_ios.h`)
3. Call from Swift: `import ZedraFFI; my_rust_callback(...)`

## Key Configuration

### Deployment Target

Must match in `scripts/build-ios.sh` (`IPHONEOS_DEPLOYMENT_TARGET`), `ios/project.yml` (`deploymentTarget.iOS`), and Xcode. Current: `16.0`.

### Xcode Rust Build Target

`ios/project.yml` adds a `ZedraRustFFI` aggregate target that calls
`scripts/xcode-build-rust.sh` and declares `ZedraFFI.xcframework` as its output.
The app target depends on `ZedraRustFFI`, so Rust rebuilds before Xcode processes
the XCFramework for linking when building from Xcode. The wrapper loads
`~/.cargo/env`, prepends common Homebrew/Cargo paths, reads Xcode's
`PLATFORM_NAME`, `EFFECTIVE_PLATFORM_NAME`, `SDK_NAME`, `CONFIGURATION`, and
`IPHONEOS_DEPLOYMENT_TARGET`, then invokes `build-ios.sh --device` or
`build-ios.sh --sim` with `--release` when needed.

`run-ios.sh` prebuilds only the selected Rust slice and then sets
`ZEDRA_SKIP_RUST_XCODE_BUILD=1` for its `xcodebuild` call to avoid a duplicate
Rust build. It also regenerates the Xcode project only when `ios/project.yml` is
newer than `ios/Zedra.xcodeproj/project.pbxproj` unless
`ZEDRA_FORCE_XCODEGEN=1` is set.

Set `ZEDRA_SKIP_RUST_XCODE_BUILD=1` for command-line native-only builds.
Set `ZEDRA_IOS_BUILD_FLAGS` in a scheme environment to pass extra flags such
as `--preview` or `--debug`. Release builds reject `--debug` and
`--debug-telemetry`.

### Release Builds

In Xcode, choose the `Zedra Release` scheme to build, install, and run the
Release configuration on a connected device.

Release builds use Cargo's `release` profile, which enables ThinLTO, one
codegen unit, symbol stripping, and `panic = "abort"` from the workspace
`Cargo.toml`. The Xcode Release configuration strips the installed product,
uses whole-module Swift optimization, disables testability/assertions, keeps
`dwarf-with-dsym` output for symbol uploads, and validates the product during
archive/export.

### Linker Flags

```yaml
OTHER_LDFLAGS: ["$(inherited)", "-ObjC", "-all_load"]
```

`$(inherited)` is required before `-ObjC -all_load` (Firebase static pods need it).

### Code Signing

Automatic in `ios/project.yml`. Pass `-allowProvisioningUpdates` to xcodebuild.

## Known Pitfalls

- **Stale xcframework**: Xcode builds refresh the matching Rust slice through the `ZedraRustFFI` dependency target. If you skip that target or use `--no-build`, run `build-ios.sh` after Rust changes.
- **New extern "C" call**: must add weak stub in `crates/zedra/src/ios_stub.c` or staticlib will be stale.
- **xcodegen required**: after editing `project.yml`, run `cd ios && xcodegen generate`. `run-ios.sh` does this automatically only when `ios/project.yml` is newer than the generated project; set `ZEDRA_FORCE_XCODEGEN=1` to force regeneration.
- **Firebase config**: Debug builds work without `ios/Zedra/GoogleService-Info.plist`; Release builds require it.
- **Metal device**: iOS uses `system_default()` directly. macOS `isRemovable()`/`isLowPower()` selectors crash on iOS.
- **`with_input_handler` log spam**: harmless — UIKit queries text input before IosWindow is registered.
