fix(ios-chat): dismissable keyboard via swipe + toolbar button (#51)

Pre-fix the iOS composer's TextField had no keyboard dismissal:
no @FocusState, no scrollDismissesKeyboard, no keyboard accessory.
With axis: .vertical + submitLabel: .send the Return key inserts a
newline rather than committing, so once the keyboard rose it stayed
up — hiding the top-trailing toolbar button on small phones.

Three additive changes:
- @FocusState private var composerFocused on ChatView, bound to the
  TextField via .focused($composerFocused).
- .scrollDismissesKeyboard(.interactively) on the message list
  ScrollView so dragging the messages downward collapses the keyboard
  with the gesture (the standard iOS chat pattern the reporter
  explicitly named — "swipe away").
- ToolbarItemGroup(placement: .keyboard) accessory with a
  keyboard.chevron.compact.down "Done" button so dismissal is also
  available without a scrollable area (e.g. fresh empty-state chat
  before any messages exist).

ScarfGo iOS only. Mac unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-27 13:38:00 +02:00
parent bb33a39b42
commit f9a288ac6c
+29
View File
@@ -27,6 +27,12 @@ struct ChatView: View {
@State private var controller: ChatController
@State private var showProjectPicker = false
@State private var showSlashCommandsSheet = false
/// Drives the composer's keyboard. Bound to the TextField via
/// `.focused(...)`; cleared by the scroll-to-dismiss gesture on
/// the message list AND by an explicit keyboard-toolbar button.
/// (issue #51 pre-fix the keyboard could never be dismissed,
/// blocking access to the toolbar nav button on small phones.)
@FocusState private var composerFocused: Bool
init(config: IOSServerConfig, key: SSHKeyBundle) {
self.config = config
@@ -234,6 +240,11 @@ struct ChatView: View {
// which fought with the user's own scroll gestures.
.defaultScrollAnchor(.bottom)
.defaultScrollAnchor(.bottom, for: .sizeChanges)
// Drag the messages downward to interactively collapse the
// keyboard the standard iOS chat gesture. Without this the
// keyboard could never be dismissed once it rose, hiding the
// top-trailing nav button on small phones (issue #51).
.scrollDismissesKeyboard(.interactively)
}
@ViewBuilder
@@ -311,9 +322,27 @@ struct ChatView: View {
.lineLimit(1...5)
.disabled(controller.state != .ready)
.submitLabel(.send)
.focused($composerFocused)
.onSubmit {
Task { await controller.send() }
}
// Explicit dismiss-keyboard affordance, complementing the
// interactive scroll-to-dismiss on the message list. iOS
// shows a keyboard accessory toolbar above the system
// keyboard whenever a focused TextField is on screen;
// putting a "Done" chevron there is the most-discoverable
// dismissal pattern (issue #51).
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button {
composerFocused = false
} label: {
Image(systemName: "keyboard.chevron.compact.down")
}
.accessibilityLabel("Hide keyboard")
}
}
Button {
Task { await controller.send() }