# byre for iPhone The iPhone app is the same `Byre` target as the Mac app, built for iOS: platform differences are `#if os(macOS)` / `#if os(iOS)` blocks, with the small shared aliases (`PlatformImage`, `PlatformColor`, `PlatformFont`, `PlatformView`, `touchOnly`, `deviceKind`) in `mac/Sources/Byre/Platform.swift`. Only the layout is the phone's own (`Phone.swift`): a tab bar with Lead, Tasks and More. Tasks lists every project's active tasks with the Mac's own task rows; a task opens pushed, with the Mac's task bar and tab strip on top and the tab's view below in place of the Mac's attached sheet. Zoomed images and diagrams cover the screen. More holds Status (shown first), Notes, Workflows and Settings. Every action that focuses a task or project (a row, a link, a notification, the switcher) bumps `Store.reveals`, which brings the Tasks tab forward with the task open; the server's own focus changes don't. The phone holds the client connection (`Remote`) like a Mac, so agents can show it pages and files and drive its Page view (`show_page`, `show_file`, `drive_page`); it answers every other client request with an error. It sends `Byre-Device: iPhone` (Macs send `Mac`), and the server signs commits and runs `byre client-exec` without `--client` only on Macs. Left out on the phone: the Server tab and running byre locally, the shell, file, hands and signing client requests, the Sparkle updater, and keyboard shortcuts. Notifications come from the app's own event stream, so they only show while the app is open; push notifications would need APNs on the server. ## Building `mac/phone.sh` builds the app with `xcodebuild` (a Swift package can't be built for iOS by `swift build`) in the same shared directory and under the same lock as `mac/bundle.sh`, assembles `Byre.app` itself (icon through `actool`, fonts, `Info.plist` from `mac/phone/Info.plist` plus the version, commit and `DT*` keys App Store Connect checks), signs it ad hoc with `mac/phone/byre.entitlements`, and wraps it in `mac/.build/phone/iphoneos/Byre.xcarchive`. Release only, like the Mac app. `xcodebuild -exportArchive -archivePath -exportOptionsPlist mac/phone/export.plist -exportPath -allowProvisioningUpdates` re-signs it for the App Store and uploads it to TestFlight, using the Apple account Xcode is signed into: Xcode makes the distribution certificate and profile itself, so none are kept in the repo. Uploading needs the App ID `run.fc.byre.ios` (iCloud with the container `iCloud.run.fc.byre.mac`, and Push Notifications) and its app record in App Store Connect; neither can be made through Apple's API. The version is the last release tag's (App Store Connect takes only numbers) and the build number is the Unix time of the build (`docs/operations.md`, "Releases"). App Store Connect refuses a build number that isn't higher than every earlier upload, which a build time always is, so the same commit can be uploaded again; a commit count wouldn't do, since it can go down when a branch with many commits uploads before its squash merge lands on main. iOS finds the app's App Intents (the Run workflow shortcut the Action button can run) only through `Byre.app/Metadata.appintents`. Xcode makes that bundle for app targets but not for a Swift package's executable, so `mac/phone.sh` runs `appintentsmetadataprocessor` itself, inside the build lock, on the source list and `.swiftconstvalues` the compiler leaves in the derived data. It has to read the sources from the shared copy the build compiled, which the next build of another worktree replaces. The simulator has no Shortcuts app, so an intent can only be tried on a phone. `mac/phone.sh --simulator` builds an ad-hoc-signed app for the iOS simulator instead, at `mac/.build/phone/iphonesimulator/Byre.app`; install it with `xcrun simctl install ` and start it with `xcrun simctl launch run.fc.byre.ios`. The simulator ignores entitlements given to `codesign` and refuses to launch an ad-hoc app signed with restricted ones, so the script links `mac/phone/byre.entitlements` into the binary's `__TEXT,__entitlements` section, as Xcode does; without the iCloud entitlement CloudKit raises at launch. Its node signs in to Tailscale like any other: `xcrun simctl launch --console-pty` prints libtailscale's sign-in URL, which changes on every launch. The simulator's window (Xcode's Device Hub) lists the app's own accessibility elements, so hands' `ax_tree` and `act` `press` drive the app without a mouse, though the frames it gives those elements are wrong; take tap points from a `capture` of the window. Hands' mouse steps work there too: a tap is a `click`, and a long press (the edit menu with Paste) is a `click` with `hold`. macOS reports Device Hub as the frontmost app with process id -1, not its window's owner, which is why hands decides a window is in front by the on-screen window order. `xcrun simctl pbcopy` leaves the simulator's clipboard empty; set this Mac's clipboard and run `xcrun simctl pbsync host booted` instead, which also carries images. `xcrun simctl io booted screenshot --display LCD ` captures the app at full resolution. To put clipboard types the Mac can't produce (such as `com.apple.uikit.image` alone), compile a small UIKit command-line program with `xcrun --sdk iphonesimulator swiftc -target arm64-apple-ios27.0-simulator` and run it with `xcrun simctl spawn booted `; it can set `UIPasteboard.general` and print its `types`. The simulator has no Photos app, and its AssistiveTouch menu draws no icons, so an iOS screenshot's Copy and Delete can't be tried there. ## Platform facts - CloudKit on iOS requires the `remote-notification` background mode in `Info.plist`, even though the app sets up no push of its own. - The phone's name is the one typed in Settings, kept in the phone's own `UserDefaults` (not synced through iCloud, since each phone has its own), and `iPhone` until one is typed. It goes in `Byre-Host` and the tailnet node's name, both fixed when the app starts. `UIDevice.current.name` returns just `iPhone` without Apple's `com.apple.developer.device-information.user-assigned-device-name` entitlement, which Apple refused because byre shows the name to other people and sends it to Tailscale and to the agents' model vendors. - The message boxes are a `UITextView` subclass (`ImageTextView`), not SwiftUI's `TextEditor`. `TextEditor`'s text view offers Paste only for text, and a `.pasteDestination` on it didn't show Paste for an iPhone screenshot's Copy and Delete on a real phone, although it did for PNG and HEIC in the simulator. The subclass offers Paste whenever `UIPasteboard.general.hasImages` and reads `UIPasteboard.general.images`, the same read as ⋯ → Paste image, which works with screenshots. The edit menu's Paste reads the clipboard without iOS's "Allow Paste" prompt; the ⋯ menu's item gets the prompt. - SwiftTerm's iOS `TerminalViewDelegate` requires `requestOpenLink`, which its macOS one provides a default for.