Phases 1-4: full VNC client implementation

- SessionController wraps RoyalVNCKit.VNCConnection via nonisolated delegate
  adapter that bridges callbacks to @MainActor; Keychain-resolved passwords;
  reconnect with jittered exponential backoff; NWPathMonitor adaptive-quality
  hook; framebuffer rendered to CALayer.contents from didUpdateFramebuffer.
- Touch + trackpad input modes with floating soft cursor overlay; hardware
  keyboard via pressesBegan/Ended → X11 keysyms; UIPointerInteraction with
  hidden cursor for indirect pointers; pinch-to-zoom; Apple Pencil as direct
  touch; two-finger pan / indirect scroll wheel events.
- Bidirectional clipboard sync (per-connection opt-in); multi-monitor screen
  picker with input remapping; screenshot capture → share sheet; on-disconnect
  reconnect/close prompt; view-only and curtain-mode persisted.
- iPad multi-window via WindowGroup(for: UUID.self) + context-menu open;
  CloudKit-backed ModelContainer with local fallback; PrivacyInfo.xcprivacy.

10 VNCCore tests + 4 VNCUI tests pass; iPhone and iPad simulator builds clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-16 20:07:54 -05:00
parent 102c3484e9
commit 1c01b3573f
28 changed files with 2359 additions and 158 deletions

View File

@@ -1,17 +1,64 @@
import SwiftUI
import SwiftData
import VNCCore
import VNCUI
@main
struct VNCApp: App {
@State private var appState = AppStateController()
private let sharedContainer: ModelContainer = {
let schema = Schema([SavedConnection.self])
let cloudKitConfiguration = ModelConfiguration(
"CloudConnections",
schema: schema,
cloudKitDatabase: .automatic
)
if let container = try? ModelContainer(for: schema,
configurations: [cloudKitConfiguration]) {
return container
}
let local = ModelConfiguration(
"LocalConnections",
schema: schema,
cloudKitDatabase: .none
)
return (try? ModelContainer(for: schema, configurations: [local]))
?? (try! ModelContainer(for: SavedConnection.self))
}()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task { await appState.initialize() }
}
.modelContainer(for: SavedConnection.self)
.modelContainer(sharedContainer)
WindowGroup("Session", for: UUID.self) { $connectionID in
DetachedSessionWindow(connectionID: connectionID)
}
.modelContainer(sharedContainer)
}
}
private struct DetachedSessionWindow: View {
let connectionID: UUID?
@Environment(\.modelContext) private var modelContext
@Query private var connections: [SavedConnection]
var body: some View {
if let id = connectionID,
let match = connections.first(where: { $0.id == id }) {
NavigationStack {
SessionView(connection: match)
}
} else {
ContentUnavailableView(
"Connection unavailable",
systemImage: "questionmark.app",
description: Text("This window's connection is no longer available.")
)
}
}
}

View File

@@ -17,13 +17,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1</string>
<string>0.4</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>4</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>Discover computers on your network that you can control remotely.</string>
<key>NSPasteboardUsageDescription</key>
<string>Sync the clipboard between this device and the remote computer when you opt in.</string>
<key>NSCameraUsageDescription</key>
<string>Optionally capture a frame of the remote screen.</string>
<key>NSBonjourServices</key>
<array>
<string>_rfb._tcp</string>
@@ -34,6 +38,8 @@
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UISupportsDocumentBrowser</key>
<false/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
@@ -51,5 +57,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIBackgroundModes</key>
<array/>
</dict>
</plist>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
</dict>
</plist>