fix(apple/tvOS): system fullscreen keyboard for all text entry — no inline fields
ci / rust (push) Has been cancelled

SwiftUI's inline TextField on tvOS is structurally wrong for television: it grows when
activated, shows a full-width editing surface behind the pill, and floats labels
off-center — none of it stylable into the Settings-app look. Per Apple's tvOS text
input guidance, real tvOS apps never edit inline: a field is a value ROW, and pressing
it raises the SYSTEM fullscreen keyboard.

- TVTextEntry (UIViewControllerRepresentable): a UITextField that becomesFirstResponder
  on appear, presenting the standard tvOS fullscreen keyboard with the field's prompt;
  done/dismiss commits the text. TVFieldRow is the Settings-style label+value lozenge.
- Add Host and PIN pairing on tvOS now use rows + keyboard covers exclusively (the
  port row also fixes the off-center value text for good — it's a Text, not a field);
  the port input validates 1...65535.
- No SwiftUI TextField remains in any tvOS code path.

Verified by screenshot: the dialog rows render exactly like the Settings app, and the
address row raises the system linear keyboard with prompt + done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 13:56:39 +02:00
parent f292b3fe3a
commit 06a2d5e0ca
3 changed files with 162 additions and 15 deletions
@@ -33,6 +33,13 @@ struct PairSheet: View {
@State private var busy = false
@State private var errorText: String?
@State private var token = CeremonyToken()
#if os(tvOS)
private enum EditField: String, Identifiable {
case pin, clientName
var id: String { rawValue }
}
@State private var editing: EditField?
#endif
var body: some View {
#if os(tvOS)
@@ -47,12 +54,12 @@ struct PairSheet: View {
.font(.callout)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
TextField("PIN", text: $pin, prompt: Text("Shown in the host's log"))
.labelsHidden()
TextField(
"Client name", text: $clientName,
prompt: Text("How the host lists this device"))
.labelsHidden()
TVFieldRow(
label: "PIN", value: pin, placeholder: "Shown in the host's log"
) { editing = .pin }
TVFieldRow(
label: "Device name", value: clientName, placeholder: "Apple TV"
) { editing = .clientName }
if let errorText {
Text(errorText)
.font(.callout)
@@ -74,6 +81,23 @@ struct PairSheet: View {
.frame(maxWidth: 1000)
.padding(60)
.onDisappear { token.cancelled = true }
.fullScreenCover(item: $editing) { field in
switch field {
case .pin:
TVTextEntry(
title: "PIN (shown in the host's log)", text: pin,
keyboardType: .numberPad
) {
pin = $0.trimmingCharacters(in: .whitespaces)
editing = nil
}
case .clientName:
TVTextEntry(title: "Device name", text: clientName) {
clientName = $0
editing = nil
}
}
}
#else
VStack(spacing: 0) {
Form {