Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.0"),
.package(url: "https://github.com/cashapp/AccessibilitySnapshot", from: "0.12.0"),
.package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols", from: "7.0.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.38.1"),
],
targets: [
.target(
Expand Down Expand Up @@ -224,6 +225,8 @@ let package = Package(
name: "ThrowCore",
dependencies: [
.target(name: "PeriscopeCore"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
],
path: "Throw/ThrowCore/Sources",
resources: [
Expand Down
14 changes: 4 additions & 10 deletions Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ProjectDescription

let destinations: Destinations = [.iPhone, .iPad]
let deployment: DeploymentTargets = .iOS("26.0")
let throwDeployment: DeploymentTargets = .iOS("27.0")

/// The Ledger menu bar app is the only native-macOS target; everything else
/// stays on the shared iOS destinations above.
Expand Down Expand Up @@ -189,7 +190,7 @@ let project = Project(
destinations: destinations,
product: .app,
bundleId: "com.stuff.throw",
deploymentTargets: deployment,
deploymentTargets: throwDeployment,
infoPlist: .extendingDefault(with: [
"CFBundleDisplayName": .string("Throw"),
"CFBundleShortVersionString": .string("0.1"),
Expand All @@ -211,14 +212,6 @@ let project = Project(
"UISceneConfigurationName": .string("Throw Controller"),
]),
]),
"UIWindowSceneSessionRoleExternalDisplayNonInteractive": .array([
.dictionary([
"UISceneConfigurationName": .string("Throw External Display"),
"UISceneDelegateClassName": .string(
"$(PRODUCT_MODULE_NAME).ExternalDisplaySceneDelegate",
),
]),
]),
]),
]),
"UIApplicationSupportsIndirectInputEvents": .boolean(true),
Expand Down Expand Up @@ -451,7 +444,7 @@ let project = Project(
destinations: destinations,
product: .unitTests,
bundleId: "com.stuff.throw.tests",
deploymentTargets: deployment,
deploymentTargets: throwDeployment,
sources: ["Throw/Throw/Tests/**"],
dependencies: [
.target(name: "Throw"),
Expand Down Expand Up @@ -640,6 +633,7 @@ let project = Project(
bundleIdSuffix: "throwcore",
productDependency: "ThrowCore",
sources: ["Throw/ThrowCore/Tests/**"],
extraPackageProducts: ["SwiftProtobuf"],
),
unitTests(
name: "ThrowUITests",
Expand Down
8 changes: 6 additions & 2 deletions Throw/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ WhereCore, RegionKit, or LifecycleKit into this feature.
`RunnableProjectionExperienceID` to playlist and runtime commands.
- Keep aircraft provider implementations in ThrowCore. ThrowUI uses the
provider-neutral operation service and domain results.
- Keep transit provider adapters in ThrowCore. Keep each city behind the typed
schedule and observation protocols. Never add agency checks to the renderer.
- Keep exactly one Transit runtime. It can poll only while Transit is active or
prewarming. Treat each realtime partition as an independent failure domain.
- Keep one experience coordinator for all scenes. Only the active and
prewarming experience runtimes may run at the same time.
- Exchange complete experience frames only while the projection is black.
Expand All @@ -48,8 +52,8 @@ WhereCore, RegionKit, or LifecycleKit into this feature.
bypass quiet output without starting a feed.
- Keep Geography offline and Map-only. Never add online map tiles or transmit
the observer location to a map provider.
- Revalidate the availability-gated iOS 27 scene-accessory adapter against the
GM SDK before release.
- Attach the iOS 27 external-display scene accessory to the controller root.
Use the shared session and production projection surface in its content.

## Installing to a device

Expand Down
58 changes: 39 additions & 19 deletions Throw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ does not import WhereCore or RegionKit and does not use LifecycleKit.

## Build and run

Generate the workspace, then use the shared `Throw` scheme on an iOS 26 or
Generate the workspace, then use the shared `Throw` scheme on an iOS 27 or
newer iPhone or iPad:

```bash
Expand All @@ -46,10 +46,9 @@ Preview runs the same projection renderer on the device.

## Projection Views

“View” is the user-facing name for a `ProjectionExperience`. Air & Space is the
first enabled View. It contains Geography, Flights, and the planned Stars and
Satellites layers. Transit is planned for nearby moving buses, trains, and
ferries. This change does not include a live transit provider.
“View” is the user-facing name for a `ProjectionExperience`. Air & Space contains
Geography, Flights, and the planned Stars and Satellites layers. Transit first
supports New York City subway trains.

One playlist controls every projector, full-screen output, and Preview. Each
View has a dwell duration. Automatic rotation starts only when two Views are
Expand All @@ -58,11 +57,31 @@ View visible until the new View has fresh data and a complete prepared frame.
Both values must belong to the same activation. The surface fades to black,
exchanges atomically, and fades back in. Two Views never share one frame.

Air & Space is the only configurable View in this release. Automatic rotation
therefore remains dormant. Transit stays in the display and preference formats
as a planned View. It has no runnable identity, so release code cannot add it to
the playlist or send it to the coordinator. A future runtime must add a new
runnable identity and update the exhaustive activation switch.
You can configure Air & Space and NYC Subway together. Then Throw can rotate
between the two Views. Each View keeps its own Map center, viewport, labels,
mark size, and context intensity.

## NYC Subway

The NYC Subway View uses the official supplemented GTFS schedule from the MTA.
It polls the eight official GTFS Realtime feed partitions every 30 seconds.
The schedule defines routes, stops, trip patterns, and route shapes. The
realtime feeds provide assigned train runs and predicted stop times.

The MTA feeds do not provide a continuous position for each train. Throw
estimates each train position along its scheduled shape. A train becomes more
certain after consecutive feed updates show its movement through stops.

Throw downloads the schedule during setup and stores it in the local cache.
It refreshes the schedule each hour while the View runs. A valid empty realtime
response completes activation. If one partition fails, the other partitions
continue to update. The failed partition keeps its last train positions for
90 seconds. Then it fades them for 30 seconds.

Transit data uses a provider-neutral schedule and observation boundary. An
additional city can supply its own GTFS adapters without changing the Transit
projection layers. The first release does not include San Francisco data,
buses, ferries, service alerts, or accessibility status.

## Aircraft sources

Expand Down Expand Up @@ -135,24 +154,25 @@ when it is inside the visible Map. True Sky always uses the observer location.
Cloud aircraft sources receive a coarse version of the Map center and the query
radius. They do not receive the exact observer location when the centers differ.

Throw bundles Natural Earth Vector 1:10m data and selected 2025 U.S. Census
Bureau data. Map rendering does not request tiles or send a location to a map
provider. The generated archive contains no place names or road names.
Throw bundles Natural Earth Vector 1:10m data, selected 2025 U.S. Census Bureau
data, and NYC's source-resolution planimetric shoreline. Map rendering does not
request tiles or send a location to a map provider. The generated archive
contains no place names or road names.

The data is generalized and is not authoritative. Natural Earth boundaries use
the default de facto view. Census boundaries support statistical work and are
not legal land descriptions. See the [Natural Earth
terms](https://www.naturalearthdata.com/about/terms-of-use/) and the [2025
TIGER/Line documentation](https://www2.census.gov/geo/pdfs/maps-data/data/tiger/tgrshp2025/TGRSHP2025_TechDoc_Ch1.pdf).
NYC shoreline geometry is from the [NYC Planimetric Database](https://data.cityofnewyork.us/Recreation/NYC-Planimetric-Database-Shoreline/59xk-wagz).

## External scenes

iOS 26 discovers noninteractive external displays through the declared scene
role. On iOS 27, the controller also registers a retained
`UISceneAccessory.externalNonInteractive` adapter. The iOS 27 integration was
compiled against the installed beta SDK and must be revalidated against the
iOS 27 GM SDK before release. Focus, keystone, and optical registration remain
projector responsibilities.
Throw requires iOS 27. The controller root attaches an
`ExternalNonInteractiveAccessory`. The system creates its external scene when
a compatible display connects. The accessory shows the same production
`ProjectionSurface` that Preview and the full-screen fallback use. Focus,
keystone, and optical registration remain projector responsibilities.

See [`AGENTS.md`](AGENTS.md) for the feature's editing rules and each module's
README for its public API and limitations.
29 changes: 11 additions & 18 deletions Throw/TODOs.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ owned by the root [`TODOs.md`](../TODOs.md).
240 NM on each projector aspect ratio. Record the devices, OS builds,
projectors, provider states, and results in the release checklist. (human
2026-08-24)
- test(Throw) [needs-design]: Revalidate and physically exercise the iOS 27
external-scene accessory against the GM SDK — the availability-gated adapter
constructs, retains, unregisters, and migrates `UISceneAccessory` registration
between controller scenes (`Throw/Sources/ThrowApp.swift:36-119`), while the
feature contract explicitly treats final-SDK validation as a release gate
(`AGENTS.md:28-29`). Confirm API compatibility, controller-window
closure/recreation, and that the iOS 26 manifest and iOS 27 accessory paths do
not create duplicate output scenes or polling demand. (human 2026-08-24)
- test(Throw) [needs-design]: Physically exercise the native iOS 27 external
scene accessory — the controller root attaches one
`ExternalNonInteractiveAccessory` and gives it the shared session
(`Throw/Sources/ThrowRuntime.swift`). Confirm USB-C and AirPlay connection,
controller-window recreation, display reconnection, resolution changes, and
single polling demand across all output scenes. (human 2026-08-24)
- test(ThrowCore) [needs-design]: Revalidate the externally controlled aircraft
provider contracts immediately before each beta release — implementation was
checked on 2026-08-24 against the current ADS-B Exchange Personal/RapidAPI
Expand All @@ -51,14 +49,9 @@ owned by the root [`TODOs.md`](../TODOs.md).

## P1s (Should do)

- feat(Throw) [needs-design]: Implement the planned Transit View — the catalog
reserves Network and Vehicles layers
(`ThrowCore/Sources/ProjectionExperience.swift:67-74`), and Views presents
Transit as unavailable
(`ThrowUI/Sources/Settings/ProjectionViewsSettingsView.swift:38-42,95-102`).
Select a live provider, define GTFS or equivalent route geometry, and add
setup and credentials if required. Give its Map runtime independent polling.
Keep vehicles brighter than dim network and Geography context. (human
2026-08-26)

# Completed issues

- feat(Throw): Implement the first Transit View with the official NYC Subway
static and realtime MTA feeds. Keep the source seams independent of the city,
and keep route lines and train marks in the generic projection pipeline.
(completed 2026-08-27)
16 changes: 7 additions & 9 deletions Throw/Throw/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The Throw app is the iOS composition and scene shell; see
- Expose the shared session through the runtime protocol. Compose concrete
controller and projection roots at each scene without `AnyView` erasure.
- Construct `ThrowRuntime` only in `ThrowRuntime.swift`. `AppDelegate` obtains
that one live runtime. Scene delegates use the platform handoff and never create a fallback.
that one live runtime. Never create a fallback runtime.
- Start cold launch from the process runtime. Never attach launch ownership to a
scene or SwiftUI task.
- Compose controller and projection surfaces through the exhaustive session
Expand All @@ -23,17 +23,15 @@ The Throw app is the iOS composition and scene shell; see
- Retain the final-background preference flush under one injected UIKit
execution lease. End the lease on completion or expiration. Cancel the
retained flush task when the lease expires or a controller returns foreground.
- Host every projected output with ThrowUI's `ThrowProjectionRootView`; keep its
UIKit window and hosting view opaque black.
- Derive size and aspect changes from the connected `UIWindowScene`, never
`UIScreen.main`.
- Retain the iOS 27 `UISceneAccessory` and its registration for as long as the
controller scene is eligible. Revalidate this adapter against the GM SDK.
- Host every projected output with ThrowUI's `ThrowProjectionRootView`. Keep its
surface opaque black.
- Attach the iOS 27 `ExternalNonInteractiveAccessory` to the controller root.
Never construct another runtime or session in the accessory content.
- Keep required-reason API declarations in `PrivacyInfo.xcprivacy`. Preserve
the built-app manifest guard when changing app resources or preferences.
- Restore the process's prior idle-timer state when the final output leaves.

## Testing

Run `./test ThrowTests`. App tests prove the delegate and every scene handoff
use the same runtime and that duplicate output IDs do not duplicate demand.
Run `./test ThrowTests`. App tests prove the shared runtime, controller-scene
lifecycle, output demand, background flush, and idle-timer contracts.
17 changes: 7 additions & 10 deletions Throw/Throw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

This target is the thin iOS shell for Throw. `ThrowRuntime.swift` is the only
runtime construction owner. `AppDelegate` obtains exactly one live runtime.
SwiftUI controller windows and UIKit-created external-display windows all
receive that runtime's shared ThrowUI session.
SwiftUI controller windows and the external-display accessory receive that
runtime's shared ThrowUI session.

Each scene composes its concrete ThrowUI root from that session. Runtime
handoff does not erase roots to `AnyView` or construct feature services.
Expand All @@ -16,15 +16,13 @@ black until the state contains loaded setup and credential status.

## Scene paths

- The iOS 26 scene manifest declares the controller and noninteractive
external-display roles.
- The app requires iOS 27.
- Each controller root binds to its exact `UIWindowScene` and forwards that
scene's foreground, background, and disconnect notifications to the shared
runtime under a typed persistent identity.
- iOS 27 controller hosting registers a retained external scene accessory,
availability-gated at runtime.
- `ExternalDisplaySceneDelegate` hosts `ThrowProjectionRootView` in a black
`UIHostingController`. The root creates `ProjectionSurface` only after launch.
- The controller root attaches an `ExternalNonInteractiveAccessory`.
- The accessory hosts `ThrowProjectionRootView` on an opaque-black surface.
The root creates `ProjectionSurface` only after launch.
- Preview and explicit full-screen mirroring fallback remain ThrowUI flows and
use the same surface.

Expand Down Expand Up @@ -55,5 +53,4 @@ duplicate static copy across the ThrowUI boundary.
## Build and test

Run the shared `Throw` scheme after `./ide --no-open`. App-shell tests are in
`ThrowTests`; domain and UI tests live with their modules. Revalidate the iOS
27 scene-accessory calls against the GM SDK before a release build.
`ThrowTests`. Domain and UI tests live with their modules.
Loading
Loading