RoomIdentity.derive() in TetherKit: ubiquityIdentityToken (iCloud account, portable across devices) → IOKit/identifierForVendor (hardware, device-tied) → Keychain UUID (last resort). No user config needed. Both apps auto-connect on relaunch if server URL was previously saved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
2.3 KiB
Swift
77 lines
2.3 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import UIKit
|
|
import TetherKit
|
|
|
|
/// Observable bridge between `TetherClient` (transport) and SwiftUI.
|
|
/// Owns the receive task and the local clipboard side-effects.
|
|
@MainActor @Observable
|
|
final class TetherStore {
|
|
var serverURL: String = UserDefaults.standard.string(forKey: "serverURL") ?? "http://tether.lan:8765"
|
|
var room: String = RoomIdentity.derive()
|
|
var feed: [Message] = []
|
|
var connected = false
|
|
var lastError: String?
|
|
|
|
private let deviceID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
|
private var client: TetherClient?
|
|
private var receiveTask: Task<Void, Never>?
|
|
|
|
func connectIfSaved() {
|
|
guard UserDefaults.standard.string(forKey: "serverURL") != nil else { return }
|
|
connect()
|
|
}
|
|
|
|
func connect() {
|
|
guard let base = URL(string: serverURL.trimmingCharacters(in: .whitespaces)),
|
|
!room.isEmpty else {
|
|
lastError = "Set a server URL first."
|
|
return
|
|
}
|
|
UserDefaults.standard.set(serverURL, forKey: "serverURL")
|
|
disconnect()
|
|
|
|
let cfg = TetherClient.Config(baseURL: base, room: room, from: deviceID)
|
|
let c = TetherClient(config: cfg)
|
|
client = c
|
|
connected = true
|
|
lastError = nil
|
|
receiveTask = Task {
|
|
for await msg in await c.clipboardStream() {
|
|
feed.insert(msg, at: 0)
|
|
if feed.count > 100 { feed.removeLast() }
|
|
}
|
|
connected = false
|
|
}
|
|
}
|
|
|
|
func disconnect() {
|
|
receiveTask?.cancel()
|
|
receiveTask = nil
|
|
connected = false
|
|
}
|
|
|
|
/// Read the system pasteboard (foreground only — iOS bans background reads)
|
|
/// and push it to the room.
|
|
func sendClipboard() {
|
|
guard let text = UIPasteboard.general.string, !text.isEmpty else {
|
|
lastError = "Clipboard is empty."
|
|
return
|
|
}
|
|
send(text)
|
|
}
|
|
|
|
func send(_ text: String) {
|
|
guard let c = client else { return }
|
|
Task {
|
|
do { try await c.send(text) }
|
|
catch { lastError = "Send failed: \(error.localizedDescription)" }
|
|
}
|
|
}
|
|
|
|
/// Copy a received message back onto the local pasteboard.
|
|
func copyToPasteboard(_ msg: Message) {
|
|
UIPasteboard.general.string = msg.text
|
|
}
|
|
}
|