feat(apple): native SwiftUI app for iOS and macOS

Add a native SwiftUI VniDrop app (Send/Receive/Settings) talking to the
Rust core via generated UniFFI Swift bindings, plus the uniffi-bindgen
helper crate. iOS uses a TabView, macOS a NavigationSplitView sidebar.
This commit is contained in:
2026-07-18 22:06:30 +02:00
parent 601cbc486e
commit ea376a382b
68 changed files with 8650 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
import SwiftUI
enum NfcShareAvailability { case available, unavailable, hidden }
/// Invitation delivery actions, ported from `TransferShareActions` (iosMain).
/// Platform implementations perform export, native share, and NFC write.
@MainActor
protocol TransferShareActions: AnyObject {
var canUseNativeShare: Bool { get }
var nfcAvailability: NfcShareAvailability { get }
func exportInvitation(ticket: String, transferName: String, onResult: @escaping (Result<Void, Error>) -> Void)
func shareInvitation(ticket: String, transferName: String, onResult: @escaping (Result<Void, Error>) -> Void)
func writeInvitationToNfc(ticket: String, onResult: @escaping (Result<Void, Error>) -> Void)
func cancelNfcWrite()
}
/// The QR + delivery buttons for a transfer's share panel, ported from the button
/// stack in `TransferSharePanel` (`TransferDetails.kt`).
struct ShareActionsView: View {
@Environment(\.vniColors) private var colors
@ObservedObject var model: SendModel
let transfer: Transfer
let ticket: String
@State private var actions: TransferShareActions = makePlatformShareActions()
@State private var writingNfc = false
var body: some View {
VStack(spacing: 12) {
if actions.nfcAvailability != .hidden {
SecondaryButton(
title: writingNfc ? String(localized: "transfer_nfc_waiting") : String(localized: "button_write_nfc"),
action: {
writingNfc = true
actions.writeInvitationToNfc(ticket: ticket) { result in
writingNfc = false
model.onInvitationResult(.nfc, result)
}
},
enabled: actions.nfcAvailability == .available && !writingNfc
)
if actions.nfcAvailability == .unavailable {
Text(LocalizedStringKey("transfer_nfc_unavailable"))
.font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter)
}
}
SecondaryButton(title: String(localized: "button_download_invitation"), action: {
actions.exportInvitation(ticket: ticket, transferName: transfer.transferName ?? "") {
model.onInvitationResult(.export, $0)
}
})
PrimaryButton(title: String(localized: "button_native_share"), action: {
actions.shareInvitation(ticket: ticket, transferName: transfer.transferName ?? "") {
model.onInvitationResult(.share, $0)
}
}, enabled: actions.canUseNativeShare)
}
.onDisappear { actions.cancelNfcWrite() }
}
}