diff --git a/docs/img/iphone-tabs.jpg b/docs/img/iphone-tabs.jpg new file mode 100644 index 0000000..3d1e6ed Binary files /dev/null and b/docs/img/iphone-tabs.jpg differ diff --git a/docs/iphone.md b/docs/iphone.md index cc60d6a..991260f 100644 --- a/docs/iphone.md +++ b/docs/iphone.md @@ -67,7 +67,21 @@ signing team in Xcode (Signing & Capabilities). ## Verified -In the iOS Simulator (iOS 26.5) and against a Frame (2026-09-27): the page and -all its reads, headset capture, live video, and file upload through the -on-Frame server. Not yet exercised: pairing with the password, Android display -changes through podman, and power actions. +The four tabs in the iPhone app, connected to a Frame + +In the iOS Simulator (iOS 26.5) against a real Frame, 2026-09-27: the app connected +with its key, copied the bundle over SFTP, started the server on the Frame and +showed all four tabs with live data. In the app's web view, Capture returned a +headset still and Live played H.264 video at 31 fps (WebCodecs works in +WKWebView). Through the app's tunnel: status, games, Steam library, Android apps, +screenshots, a file upload (checked on the Frame), a background install job, and +the power password check (a wrong password is refused). The server on the Frame +exits within seconds of the app closing. + +Not yet exercised: pairing with the password (only the add-the-key route), +Android display changes through podman (no Android app was running), a real +sleep/restart/shut down, and a physical iPhone. + +Debug builds have Simulator test hooks (`FRAME_TEST_HOST`, `FRAME_TEST_PAGE`, +`FRAME_TEST_JS`, and the tunnel URL in the app's Caches folder); release builds +don't. diff --git a/ios/FrameControl/App/AppModel.swift b/ios/FrameControl/App/AppModel.swift index 018d333..27ee3d8 100644 --- a/ios/FrameControl/App/AppModel.swift +++ b/ios/FrameControl/App/AppModel.swift @@ -178,7 +178,16 @@ final class AppModel: ObservableObject { self.server = server self.forwarder = f readySince = Date() - phase = .ready(URL(string: "http://127.0.0.1:\(f.localPort)/?key=\(key)")!) + var page = "http://127.0.0.1:\(f.localPort)/?key=\(key)" + #if DEBUG + // Test hooks for the Simulator: open on a given tab, and leave the URL where + // a test can drive the same tunnel (`simctl get_app_container … data`). + if let tab = ProcessInfo.processInfo.environment["FRAME_TEST_PAGE"] { page += "#\(tab)" } + if let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { + try? page.write(to: dir.appendingPathComponent("frame-test-url.txt"), atomically: true, encoding: .utf8) + } + #endif + phase = .ready(URL(string: page)!) // Runs at once if it stopped in the moment since the check above. server.whenExited { [weak self] tail in Task { @MainActor in self?.lost(mine, "Frame Control on the headset stopped. \(tail.suffix(200))") } diff --git a/ios/FrameControl/Web/WebShell.swift b/ios/FrameControl/Web/WebShell.swift index afd23e4..fee7df2 100644 --- a/ios/FrameControl/Web/WebShell.swift +++ b/ios/FrameControl/Web/WebShell.swift @@ -144,6 +144,14 @@ struct WebShell: UIViewRepresentable { (root.presentedViewController ?? root).present(sheet, animated: true) } + #if DEBUG + /// Simulator test hook: FRAME_TEST_JS runs in the page once it has loaded. + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard let js = ProcessInfo.processInfo.environment["FRAME_TEST_JS"] else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { webView.evaluateJavaScript(js) } + } + #endif + // MARK: navigation: the app's page stays here; other sites open in Safari func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,