fix(apple/tvOS): pushed routes instead of modal covers — the Settings-app navigation feel
ci / rust (push) Has been cancelled

Add Host, Settings and PIN pairing were fullScreenCover overlays, which is why
navigating felt unlike the system Settings app (no push animation, no Menu-pops-a-level
semantics). They are now navigationDestination ROUTES pushed inside the home
NavigationStack:

- the system push/pop animation and Menu-button back navigation come for free;
- the Settings pickers' navigationLink pushes reuse the same stack (its inner
  NavigationStack wrapper is gone, as is the tvOS Done row — Menu pops, like Settings);
- Add Host is a real full-screen page (system navigation title, Settings-style rows on
  the standard backdrop) instead of a floating dialog, same for the pairing page;
- the thickMaterial cover backdrops became unnecessary and are gone. The system
  keyboard entries stay as covers — that presentation is system-owned either way.

iOS/macOS keep their sheets. Verified by screenshot: Add Host renders as a pushed
full-screen route with the title top-center.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:03:10 +02:00
parent 06a2d5e0ca
commit f01b07a973
4 changed files with 23 additions and 34 deletions
@@ -23,8 +23,6 @@ struct AddHostSheet: View {
// No inline text editing on tvOS Settings-style value rows; pressing one
// raises the SYSTEM fullscreen keyboard (TVTextEntry).
VStack(spacing: 24) {
Text("Add Host")
.font(.title3.weight(.semibold))
TVFieldRow(
label: "Name", value: name, placeholder: "Optional"
) { editing = .name }
@@ -43,6 +41,7 @@ struct AddHostSheet: View {
}
.frame(maxWidth: 1000)
.padding(60)
.navigationTitle("Add Host")
.fullScreenCover(item: $editing) { field in
switch field {
case .name:
@@ -55,18 +55,7 @@ struct ContentView: View {
// On the outer Group so the sheet survives the trust-prompt home transition
// (the "Pair with PIN instead" path disconnects first the host's accept loop
// is sequential, a pairing connection would queue behind the live session).
#if os(tvOS)
.fullScreenCover(item: $pairingTarget) { host in
PairSheet(host: host) { fingerprint in
guard pairingTarget?.id == host.id else { return }
store.pin(host.id, fingerprint: fingerprint)
var pinned = host
pinned.pinnedSHA256 = fingerprint
connect(pinned)
}
.background(.thickMaterial, ignoresSafeAreaEdges: .all)
}
#else
#if !os(tvOS)
.sheet(item: $pairingTarget) { host in
PairSheet(host: host) { fingerprint in
// Backstop against a stale ceremony surfacing after dismissal (PairSheet
@@ -149,6 +138,25 @@ struct ContentView: View {
}
}
.navigationTitle("Punktfunkempfänger")
#if os(tvOS)
// Pushed routes the Settings-app navigation feel (push animation, Menu
// pops) instead of modal overlays.
.navigationDestination(isPresented: $showAddHost) {
AddHostSheet { store.add($0) }
}
.navigationDestination(isPresented: $showSettings) {
SettingsView()
}
.navigationDestination(item: $pairingTarget) { host in
PairSheet(host: host) { fingerprint in
guard pairingTarget?.id == host.id else { return }
store.pin(host.id, fingerprint: fingerprint)
var pinned = host
pinned.pinnedSHA256 = fingerprint
connect(pinned)
}
}
#endif
#if !os(tvOS)
.toolbar {
#if os(iOS)
@@ -173,20 +181,7 @@ struct ContentView: View {
#if os(macOS)
.frame(minWidth: 480, minHeight: 360)
#endif
#if os(tvOS)
// tvOS forms/lists have CLEAR backgrounds and a cover only shows what the
// presented view paints back them with the standard tv blur-over-content.
.fullScreenCover(isPresented: $showAddHost) {
AddHostSheet { store.add($0) }
.background(.thickMaterial, ignoresSafeAreaEdges: .all)
}
.fullScreenCover(isPresented: $showSettings) {
NavigationStack {
SettingsView()
}
.background(.thickMaterial, ignoresSafeAreaEdges: .all)
}
#else
#if !os(tvOS)
.sheet(isPresented: $showAddHost) {
AddHostSheet { store.add($0) }
}
@@ -44,9 +44,6 @@ struct PairSheet: View {
var body: some View {
#if os(tvOS)
VStack(spacing: 24) {
Label("Pair with \(host.displayName)", systemImage: "lock.shield")
.font(.title3.weight(.semibold))
.foregroundStyle(.tint)
Text("The host prints the PIN when pairing is armed "
+ "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "
+ "Pairing verifies both sides at once — no fingerprint comparison "
@@ -80,6 +77,7 @@ struct PairSheet: View {
}
.frame(maxWidth: 1000)
.padding(60)
.navigationTitle("Pair with \(host.displayName)")
.onDisappear { token.cancelled = true }
.fullScreenCover(item: $editing) { field in
switch field {
@@ -87,9 +87,6 @@ struct SettingsView: View {
.font(.caption)
.foregroundStyle(.secondary)
}
Section {
Button("Done") { dismiss() }
}
}
.navigationTitle("Settings")
}