Files
vnidrop/apple/VniDrop/Platform/FileSystemService+iOS.swift
cdricms ea376a382b 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.
2026-07-18 22:06:30 +02:00

99 lines
3.3 KiB
Swift

#if os(iOS)
import Foundation
import UIKit
import VnidropCore
/// iOS file system service, ported from `FileSystemService.ios.kt`.
/// App-owned Documents is the fixed receive folder; custom folders are not
/// supported because raw external picker URLs do not survive relaunch.
struct IosFileSystemService: FileSystemService {
var supportsCustomReceiveFolders: Bool { false }
func defaultReceiveFolder() -> ReceiveFolder {
let path = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?.path ?? ""
return ReceiveFolder(kind: .fileSystemPath, value: path, displayName: "Documents")
}
func validateReceiveFolder(_ folder: ReceiveFolder) async -> FolderAccessStatus {
switch folder.kind {
case .fileSystemPath:
return FileManager.default.isWritableFile(atPath: folder.value) ? .writable : .unavailable
case .iosSecurityScopedUrl:
return validateSecurityScopedUrl(folder.value)
}
}
func canRevealReceiveFolder(_ folder: ReceiveFolder) -> Bool {
folder.kind == .fileSystemPath
&& folder.value.trimmingTrailingSlash == defaultReceiveFolder().value.trimmingTrailingSlash
}
func revealReceiveFolder(_ folder: ReceiveFolder) async -> Result<Void, Error> {
guard canRevealReceiveFolder(folder) else {
return .failure(InvitationError.message("The receive folder is not VniDrop Documents"))
}
guard let url = URL(string: "shareddocuments://\(folder.value)") else {
return .failure(InvitationError.message("The Files location URL is unavailable"))
}
let opened = await withCheckedContinuation { continuation in
DispatchQueue.main.async {
UIApplication.shared.open(url, options: [:]) { success in
continuation.resume(returning: success)
}
}
}
return opened ? .success(()) : .failure(InvitationError.message("Could not open VniDrop Documents in Files"))
}
func discardPickedFiles(_ files: [PickedShareFile]) async {
let paths = Set(files.filter { $0.isTemporaryCopy }.map { $0.value })
for path in paths {
try? FileManager.default.removeItem(atPath: path)
}
}
func sharePickedFiles(
repository: CoreRepository,
files: [PickedShareFile],
transferName: String,
senderName: String,
accessPolicy: ShareAccessPolicy
) async -> Result<Share, Error> {
guard !files.isEmpty else {
return .failure(InvitationError.message("Select at least one file to share"))
}
let sources = files.map { $0.toIosShareSource() }
return await repository.shareSources(
sources, transferName: transferName, senderName: senderName, accessPolicy: accessPolicy
)
}
private func validateSecurityScopedUrl(_ value: String) -> FolderAccessStatus {
let url = URL(string: value) ?? URL(fileURLWithPath: value)
let started = url.startAccessingSecurityScopedResource()
defer { if started { url.stopAccessingSecurityScopedResource() } }
if FileManager.default.isWritableFile(atPath: url.path) {
return .writable
}
return .permissionRequired
}
}
extension PickedShareFile {
/// iOS shares by filesystem path (from `asCopy` picker temp files).
func toIosShareSource() -> ShareSource {
ShareSource(kind: .path, value: value, displayName: displayName, isDirectory: isDirectory)
}
}
private extension String {
var trimmingTrailingSlash: String {
var s = self
while s.hasSuffix("/") { s.removeLast() }
return s
}
}
#endif