# VniDrop — native Apple app (iOS / iPadOS / macOS)

A native SwiftUI app for Apple platforms, sharing the existing Rust transfer core
(`crates/vnidrop`) through UniFFI-generated Swift bindings. The Rust crate is not
modified; the Kotlin/Compose app layer is ported to Swift and mirrors the Compose
UI screen-for-screen. Android, Windows, and Linux continue to use `shared/` + Compose.

## Layout

```
apple/
  scripts/build-core.sh     # builds the Rust core + generates Swift bindings + xcframework
  VnidropCore/              # local SwiftPM package: xcframework + generated Vnidrop.swift
  VniDrop/                  # SwiftUI app sources
    App/                    # entry point, object graph, root view, environment
    Core/                   # repository, models, preferences, notifications, progress
    Features/Send|Receive|Approvals|Settings/
    UI/Theme|Components|Navigation|Feedback|Shell/
    Platform/               # pickers, QR, NFC, share/export, per-OS file services
    Resources/              # Localizable.xcstrings, Info.plist, entitlements, assets
  Tests/                    # XCTest bundle (VniDropTests target)
  project.yml               # XcodeGen spec for the iOS/macOS app and test targets
```

## Build & run

Prerequisites: Xcode, Rust with the Apple targets
(`aarch64-apple-ios`, `aarch64-apple-ios-sim`, `x86_64-apple-ios`,
`aarch64-apple-darwin`), and `xcodegen` (`brew install xcodegen`).

```bash
# From the repository root:
make apple-core          # Rust core, Swift bindings, and XCFramework
make apple-project       # generate apple/VniDrop.xcodeproj
make open-apple-project  # generate and open the project in Xcode
make build-apple-macos   # unsigned macOS build (App Store target)
make open-apple          # build and launch the macOS app
make build-apple-ios     # unsigned iOS simulator app
make check-apple         # iOS simulator tests
```

`make apple-project` also generates ignored Store and Direct version xcconfig
files. Their `CURRENT_PROJECT_VERSION` values come from the central version
resolver as UTC `YYYYMMDD.HHMM.SS` build identifiers. Regenerate the project
before creating another App Store archive so it receives a fresh build number;
direct DMG builds refresh their own value automatically.

### macOS shipping channels

The macOS app ships through two targets that build identical sources:

- **`VniDrop`** (`Release`) — Mac App Store / TestFlight. Sandboxed, no
  self-updater.
- **`VniDropDirect`** (`Release-Direct`) — direct-download `.dmg` on GitHub
  Releases + Homebrew cask. Adds the **Sparkle** auto-updater behind the
  `DIRECT_DISTRIBUTION` compile flag, so the App Store binary never links Sparkle.

```bash
make build-apple-macos-direct   # unsigned compile-check of the direct target
make build-apple-dmg                # signed (+ notarized) .dmg
```

Full signing, notarization, appcast, and cask flow: see
[`RELEASE-MACOS.md`](RELEASE-MACOS.md).

Use `APPLE_PROFILE=release` to request a release Rust core, or set
`APPLE_DESTINATION` to override the automatically selected iOS simulator.
Code signing is disabled for the app and test targets; local and CI builds do
not require an Apple Development team or provisioning profile. Make builds can
opt in with `APPLE_CODE_SIGNING=YES`. For signed builds from Xcode, create the
ignored `apple/Local.xcconfig` and override the signing settings there, including
the development team.

## Typecheck & tests

The Xcode project is the only build definition: it owns the UI, its package
dependencies, and the `VniDropTests` bundle (module `VniDrop`, which is what the
tests import). Everything runs through `xcodebuild`:

```bash
make check-apple         # iOS simulator unit tests
make build-apple-macos   # unsigned macOS build (typecheck)
```

There is deliberately no SwiftPM manifest for the app. A second build definition
would duplicate the target's package dependencies, and the previous one had
already drifted out of sync with `project.yml` badly enough that neither
`swift build` nor `swift test` worked.

## Generated / ignored artifacts

`build-core.sh` produces build outputs that are gitignored (see `apple/.gitignore`):
`VnidropCore/vnidrop.xcframework/`, `VnidropCore/Sources/VnidropCore/Vnidrop.swift`,
and `.build-core/`. A clean checkout must run `build-core.sh` before generating or
opening the Xcode project. `VniDrop.xcodeproj` itself is generated by XcodeGen from
`project.yml` and does not need to be committed.

## Build profile note

The default is `debug`. The workspace `[profile.release]` uses thin LTO, which the
current macOS toolchain miscompiles into corrupt host proc-macro dylibs
("mis-aligned LINKEDIT string pool"). `build-core.sh` sets
`CARGO_PROFILE_DEV_STRIP=none` (matching the existing Gobley Xcode run-script) so
debug builds succeed. For a release core, disable LTO for proc-macros/build
scripts (e.g. add a `[profile.release.build-override] lto = false` locally) — the
Rust crate itself is never changed.

## System frameworks

The Rust core (iroh network stack) links `SystemConfiguration`, `Security`, and
`libresolv`. These are declared in `project.yml` for the app target.

## Parity & scope

Screens mirror the Compose UI in `shared/`. Two deliberate simplifications:
- Empty-state Lottie animations are rendered as SF Symbols (no `lottie-ios`
  dependency); swap in `lottie-ios` if exact-parity animation is required.
- Bug reporting is stubbed behind `BugReportService` (`NoopBugReportService`) and
  a real transport lands in a later phase. There is no telemetry or crash
  auto-reporting.
```
