Uploading the 0.24.0 set surfaced a pair screen that reads as broken. Digging in, the pair screen was the least of it — the hero had never been the orientation it claimed, and the capture was picking up whatever machine happened to be on the LAN.
What was wrong
03-pair — three faults at once. It was hand-composed into a ZStack rather than presented. PairSheet is a bottom sheet on iOS: its presentationDetents and the system's Liquid Glass background only exist inside a real .sheet. Composed, the detents were inert, the grouped Form stretched to the full height of the screen, and the capture was a strip of content over a large black void. On top of that the PIN was empty, so "Pair & Connect" rendered disabled, and the client-name field showed the capture simulator's own name — pf-shot-iphone-6.9 went out on the listing that way.
Every .landscape scene was captured in PORTRAIT.IOSOrientationConfigurator asked for the geometry update from updateUIViewController, where view.window is still nil — SwiftUI makes exactly one update pass for a .background representable, before the hierarchy is in a window, so the guard fell through and nothing ever asked again. The stream hero and the trust card both shipped at the portrait size. A real UIViewController now asks from viewDidAppear and pins supportedInterfaceOrientations.
The hero's HUD sat under the Dynamic Island — resolution, bitrate, the latency breakdown, the entire reason that screenshot exists. The shot host applied .ignoresSafeArea() to the whole scene; only the black backing ignores it now, and scenes that want full bleed already ignore it themselves.
Sheets do not inherit .environment(\.colorScheme, .dark) across the presentation boundary — they follow the device. The pairing sheet came out light grey over the dark app. The simulator is set to dark appearance now.
Discovery browsed the live LAN mid-capture, so a bystanding machine's hostname reached the listing and no two runs matched. HostDiscovery gains a debugSet seam (the counterpart to the existing HostWaker.debugSet); the mock hosts advertise, so cards read ONLINE through the real advertises path and the reachability probe never touches the network.
Created simulators were named pf-shot-<prefix>, which the reuse regex never matches — every run created another simulator and none was ever reused. They are named after the device now: reusable, and not user-visible junk.
Three bugs found on the way, none screenshot-only
HostStore/ProfileStorepersisted the harness's mock data. On a dev Mac that is the same App-Group suite the real app reads, so running the script could replace the tester's saved hosts with "Battlestation" & co.
GamepadHomeView drew the controller chip as a trailing .overlay, which reserves no width — on a portrait phone it sat directly on top of the centred "Select a Host". Laid out as a row with a hidden leading mirror.
The pairing sheet's field prompt said "How the host lists this Mac" on iPhone and iPad.
Coverage
The listing set is six scenes in listing order: the stream, the machines it found, the couch/controller mode, waking a sleeping host, the quality controls, pairing. The console and wake screens already existed in ShotScenes.all and were simply never captured. Settings opens on Display (resolution, frame rate, bitrate, HDR, codec) rather than General. Mock hosts carry OS marks, Wake-on-LAN MACs and profile chips, so the grid is full rather than three offline rows over an empty half-screen. SCENES= overrides the set for the dev scenes.
Known limits, deliberately not papered over
A .landscape scene does not rotate on iPad. A multitasking-capable iPad app is resizable, so iPadOS ignores requestGeometryUpdate, and simctl cannot rotate a simulated device. The iPad set is portrait throughout — a valid App Store size, and uniform. Documented in the script header; rotate the Simulator by hand (⌘←) for a landscape iPad hero.
The hero is still the synthetic synthwave frame and reads as a wallpaper, not a game. PUNKTFUNK_SHOT_HERO=~/frame.png already drops in a real captured frame — worth doing before the next upload.
requestGeometryUpdate now reports a refusal instead of failing silently. That does not help on the simulator (an app's stdout doesn't reach the driver through simctl launch) but it will on macOS and on a device.
Verification
swift build clean; swift test 189 passed, 0 failures, 5 remote tests self-skipped. Captured both required sizes and eyeballed every scene: 6 × iPhone 6.9" (hero 2868×1320 landscape, rest 1320×2868) and 6 × iPad 13" (2064×2752), all App Store-valid.
macOS and tvOS are unchanged and uncaptured here — CI does not run them, and they need a GUI session / the Tier-3 build-std slice respectively.
Uploading the 0.24.0 set surfaced a pair screen that reads as broken. Digging in, the pair screen was the least of it — the hero had never been the orientation it claimed, and the capture was picking up whatever machine happened to be on the LAN.
## What was wrong
**`03-pair` — three faults at once.** It was hand-composed into a `ZStack` rather than *presented*. `PairSheet` is a bottom sheet on iOS: its `presentationDetents` and the system's Liquid Glass background only exist inside a real `.sheet`. Composed, the detents were inert, the grouped Form stretched to the full height of the screen, and the capture was a strip of content over a large black void. On top of that the PIN was empty, so **"Pair & Connect" rendered disabled**, and the client-name field showed the capture simulator's own name — `pf-shot-iphone-6.9` went out on the listing that way.
**Every `.landscape` scene was captured in PORTRAIT.** `IOSOrientationConfigurator` asked for the geometry update from `updateUIViewController`, where `view.window` is still nil — SwiftUI makes exactly one update pass for a `.background` representable, before the hierarchy is in a window, so the `guard` fell through and nothing ever asked again. The stream hero and the trust card both shipped at the portrait size. A real `UIViewController` now asks from `viewDidAppear` and pins `supportedInterfaceOrientations`.
**The hero's HUD sat under the Dynamic Island** — resolution, bitrate, the latency breakdown, the entire reason that screenshot exists. The shot host applied `.ignoresSafeArea()` to the whole scene; only the black backing ignores it now, and scenes that want full bleed already ignore it themselves.
**Sheets do not inherit `.environment(\.colorScheme, .dark)`** across the presentation boundary — they follow the *device*. The pairing sheet came out light grey over the dark app. The simulator is set to dark appearance now.
**Discovery browsed the live LAN mid-capture**, so a bystanding machine's hostname reached the listing and no two runs matched. `HostDiscovery` gains a `debugSet` seam (the counterpart to the existing `HostWaker.debugSet`); the mock hosts advertise, so cards read ONLINE through the real `advertises` path and the reachability probe never touches the network.
**Created simulators were named `pf-shot-<prefix>`**, which the reuse regex never matches — every run created another simulator and none was ever reused. They are named after the device now: reusable, and not user-visible junk.
## Three bugs found on the way, none screenshot-only
- `HostStore`/`ProfileStore` **persisted the harness's mock data**. On a dev Mac that is the same App-Group suite the real app reads, so running the script could replace the tester's saved hosts with "Battlestation" & co.
- `GamepadHomeView` drew the controller chip as a trailing `.overlay`, which reserves no width — on a portrait phone it sat directly on top of the centred "Select a Host". Laid out as a row with a hidden leading mirror.
- The pairing sheet's field prompt said "How the host lists this **Mac**" on iPhone and iPad.
## Coverage
The listing set is six scenes in listing order: the stream, the machines it found, the couch/controller mode, waking a sleeping host, the quality controls, pairing. The console and wake screens already existed in `ShotScenes.all` and were simply never captured. Settings opens on **Display** (resolution, frame rate, bitrate, HDR, codec) rather than General. Mock hosts carry OS marks, Wake-on-LAN MACs and profile chips, so the grid is full rather than three offline rows over an empty half-screen. `SCENES=` overrides the set for the dev scenes.
## Known limits, deliberately not papered over
- **A `.landscape` scene does not rotate on iPad.** A multitasking-capable iPad app is resizable, so iPadOS ignores `requestGeometryUpdate`, and `simctl` cannot rotate a simulated device. The iPad set is portrait throughout — a valid App Store size, and uniform. Documented in the script header; rotate the Simulator by hand (⌘←) for a landscape iPad hero.
- **The hero is still the synthetic synthwave frame** and reads as a wallpaper, not a game. `PUNKTFUNK_SHOT_HERO=~/frame.png` already drops in a real captured frame — worth doing before the next upload.
- `requestGeometryUpdate` now reports a refusal instead of failing silently. That does not help on the simulator (an app's stdout doesn't reach the driver through `simctl launch`) but it will on macOS and on a device.
## Verification
`swift build` clean; `swift test` 189 passed, 0 failures, 5 remote tests self-skipped. Captured both required sizes and eyeballed every scene: 6 × iPhone 6.9" (hero 2868×1320 landscape, rest 1320×2868) and 6 × iPad 13" (2064×2752), all App Store-valid.
macOS and tvOS are unchanged and uncaptured here — CI does not run them, and they need a GUI session / the Tier-3 build-std slice respectively.
Uploading the 0.24.0 set surfaced a pair screen that reads as broken, and a
hero that was never the orientation it claimed.
The capture harness:
- Landscape scenes were captured in PORTRAIT. `IOSOrientationConfigurator`
asked for the geometry update from `updateUIViewController`, where
`view.window` is still nil — SwiftUI makes one update pass for a
`.background` representable, before the hierarchy is in a window, so the
guard fell through and nothing ever asked again. Both `.landscape` scenes
(the stream hero, the trust card) shipped as portrait. Now a real
UIViewController asks from `viewDidAppear` and pins
`supportedInterfaceOrientations`.
- The shot host applied `.ignoresSafeArea()` to the whole scene, so the
hero's HUD — resolution, bitrate, the latency breakdown, the entire point
of that screenshot — sat under the Dynamic Island. Only the black backing
ignores it now; scenes that want full bleed already ignore it themselves.
- `03-pair` was hand-composed into a ZStack rather than presented. PairSheet
is a bottom sheet on iOS: its detents and the system's Liquid Glass only
exist inside a real `.sheet`. Composed, the grouped Form stretched to full
screen height and the capture was a strip of content over a black void,
with a DISABLED "Pair & Connect" (empty PIN) and the capture simulator's
own name — `pf-shot-iphone-6.9` — rendered in as the device name.
- Sheets do not inherit `.environment(\.colorScheme, .dark)` across the
presentation boundary; they follow the DEVICE. The pairing sheet came out
light grey over the dark app. The simulator is now set to dark appearance.
- Discovery browsed the live LAN mid-capture, so a bystanding machine's
hostname went out on the listing and no two runs matched. `HostDiscovery`
gains a `debugSet` seam (the counterpart to `HostWaker.debugSet`); the
mock hosts advertise, so cards read ONLINE through the real `advertises`
path and the reachability probe never touches the network.
- Created simulators were named `pf-shot-<prefix>`, which the reuse regex
never matches: every run created another simulator and none was reused.
They are named after the device now — reusable, and not user-visible junk.
Two bugs found on the way, neither screenshot-only:
- HostStore/ProfileStore PERSISTED the harness's mock data. On a dev Mac
that is the same App-Group suite the real app reads, so running the
script could replace the tester's saved hosts with "Battlestation" & co.
- GamepadHomeView drew the controller chip as a trailing `.overlay`, which
reserves no width — on a portrait phone it sat on top of the centred
"Select a Host". Laid out as a row with a hidden leading mirror.
- The pairing sheet's field prompt said "How the host lists this Mac" on
iPhone and iPad.
Coverage: the listing set is six scenes in listing order, and is now the
stream, the machines it found, the couch/controller mode, waking a sleeping
host, the quality controls and pairing — the console and wake screens
already existed in `ShotScenes.all` and were simply never captured. Mock
hosts carry OS marks, Wake-on-LAN MACs and profile chips so the grid is
full rather than three offline rows over an empty half-screen. `SCENES=`
overrides the set for the dev scenes.
- Six mock hosts rather than three. An iPad-13 portrait grid is three columns
wide and 2752 px tall; three cards left ~60% of the capture as black.
- Settings opens on Display, not General. Resolution, frame rate, bitrate,
HDR and codec are what someone reads a streaming app's settings shot for.
- The wake scene is the modal-over-grid variant. The gamepad-UI one is a
full-screen takeover over a bare gradient — correct, but four lines of text
on an empty aurora; the modal shows the same overlay over the host grid.
- `requestGeometryUpdate` now reports a refusal instead of failing silently.
It does not help on the simulator (an app's stdout doesn't reach the driver
through `simctl launch`) but it will on macOS and on a device.
- Documented that `.landscape` does not rotate on iPad: a multitasking-capable
iPad app is resizable, so iPadOS ignores the request and simctl cannot
rotate a simulated device. The iPad set is portrait throughout.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Uploading the 0.24.0 set surfaced a pair screen that reads as broken. Digging in, the pair screen was the least of it — the hero had never been the orientation it claimed, and the capture was picking up whatever machine happened to be on the LAN.
What was wrong
03-pair— three faults at once. It was hand-composed into aZStackrather than presented.PairSheetis a bottom sheet on iOS: itspresentationDetentsand the system's Liquid Glass background only exist inside a real.sheet. Composed, the detents were inert, the grouped Form stretched to the full height of the screen, and the capture was a strip of content over a large black void. On top of that the PIN was empty, so "Pair & Connect" rendered disabled, and the client-name field showed the capture simulator's own name —pf-shot-iphone-6.9went out on the listing that way.Every
.landscapescene was captured in PORTRAIT.IOSOrientationConfiguratorasked for the geometry update fromupdateUIViewController, whereview.windowis still nil — SwiftUI makes exactly one update pass for a.backgroundrepresentable, before the hierarchy is in a window, so theguardfell through and nothing ever asked again. The stream hero and the trust card both shipped at the portrait size. A realUIViewControllernow asks fromviewDidAppearand pinssupportedInterfaceOrientations.The hero's HUD sat under the Dynamic Island — resolution, bitrate, the latency breakdown, the entire reason that screenshot exists. The shot host applied
.ignoresSafeArea()to the whole scene; only the black backing ignores it now, and scenes that want full bleed already ignore it themselves.Sheets do not inherit
.environment(\.colorScheme, .dark)across the presentation boundary — they follow the device. The pairing sheet came out light grey over the dark app. The simulator is set to dark appearance now.Discovery browsed the live LAN mid-capture, so a bystanding machine's hostname reached the listing and no two runs matched.
HostDiscoverygains adebugSetseam (the counterpart to the existingHostWaker.debugSet); the mock hosts advertise, so cards read ONLINE through the realadvertisespath and the reachability probe never touches the network.Created simulators were named
pf-shot-<prefix>, which the reuse regex never matches — every run created another simulator and none was ever reused. They are named after the device now: reusable, and not user-visible junk.Three bugs found on the way, none screenshot-only
HostStore/ProfileStorepersisted the harness's mock data. On a dev Mac that is the same App-Group suite the real app reads, so running the script could replace the tester's saved hosts with "Battlestation" & co.GamepadHomeViewdrew the controller chip as a trailing.overlay, which reserves no width — on a portrait phone it sat directly on top of the centred "Select a Host". Laid out as a row with a hidden leading mirror.Coverage
The listing set is six scenes in listing order: the stream, the machines it found, the couch/controller mode, waking a sleeping host, the quality controls, pairing. The console and wake screens already existed in
ShotScenes.alland were simply never captured. Settings opens on Display (resolution, frame rate, bitrate, HDR, codec) rather than General. Mock hosts carry OS marks, Wake-on-LAN MACs and profile chips, so the grid is full rather than three offline rows over an empty half-screen.SCENES=overrides the set for the dev scenes.Known limits, deliberately not papered over
.landscapescene does not rotate on iPad. A multitasking-capable iPad app is resizable, so iPadOS ignoresrequestGeometryUpdate, andsimctlcannot rotate a simulated device. The iPad set is portrait throughout — a valid App Store size, and uniform. Documented in the script header; rotate the Simulator by hand (⌘←) for a landscape iPad hero.PUNKTFUNK_SHOT_HERO=~/frame.pngalready drops in a real captured frame — worth doing before the next upload.requestGeometryUpdatenow reports a refusal instead of failing silently. That does not help on the simulator (an app's stdout doesn't reach the driver throughsimctl launch) but it will on macOS and on a device.Verification
swift buildclean;swift test189 passed, 0 failures, 5 remote tests self-skipped. Captured both required sizes and eyeballed every scene: 6 × iPhone 6.9" (hero 2868×1320 landscape, rest 1320×2868) and 6 × iPad 13" (2064×2752), all App Store-valid.macOS and tvOS are unchanged and uncaptured here — CI does not run them, and they need a GUI session / the Tier-3 build-std slice respectively.