Files
vnidrop/apple
cdricms 3469b122c2 feat(apple): local notifications for transfer lifecycle events
Adds background notifications for the "the thing you were waiting for is
done" moments, alongside the existing incoming-approval-request one:

  - a receive finished downloading            (receive -> done)
  - a receive failed / was interrupted         (receive -> failed)
  - a share you own failed                      (send -> failed)
  - a receiver finished downloading your share  (receiver status completed)

A new TransferNotificationCoordinator observes core state + signals and
publishes these; the decision of which moments notify is a pure function
(plannedTransferNotifications / plannedReceiverNotifications), unit-tested
independently. The first state snapshot only primes existing history as seen
so launch doesn't spam.

Notification permission is now the single source of truth. The in-app
notifications toggle and its decoupled UserDefaults preference are gone;
the Settings section shows an "Allow notifications" button that requests the
OS permission (or deep-links to Settings once decided), and notifications
gate purely on `permission == .granted`.

macOS delivery fixes:
  - add a UNUserNotificationCenterDelegate so banners present even while the
    app is active (the app window is usually open on macOS)
  - present-when-active on macOS, suppress-when-foregrounded on iOS
  - reserve the notification id before awaiting publish: the CombineLatest
    fired several times and re-added the same identifier, which macOS
    coalesces into a silent update with no banner
  - LocalNotificationService seeds its permission at init so gating can't
    race a not-yet-refreshed .notDetermined

Eight localized title/body strings added (apple-only); the shared
notifications_description copy is generalized from "receive requests" to
"transfer activity".
2026-07-23 23:29:40 +02:00
..

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 (ported progress-derivation assertions)
  Package.swift             # builds VniDrop/ as a library for CLI build/test
  project.yml               # XcodeGen spec for the iOS/macOS app target

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).

# 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
make open-apple          # build and launch the macOS app
make build-apple-ios     # unsigned iOS simulator build
make check-apple         # iOS simulator tests

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.

Command-line typecheck & tests

Package.swift builds the same sources as a library (minus the @main entry), so the shared logic can be checked and unit-tested without Xcode:

cd apple
swift build                       # macOS
swift test                        # runs Tests/ (ported progress-derivation assertions)
# iOS typecheck:
swift build --triple arm64-apple-ios16.0-simulator --sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)"

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 both Package.swift (for CLI build/test) and 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.
  • The full diagnostics/telemetry stack (diagnostics/*) is stubbed behind BugReportService / DiagnosticsBuildConfig and lands in a later phase; the UI hides the diagnostics toggle when not compiled in.