From f9a288ac6c8a53c093d86e739ff2366673d195d1 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Mon, 27 Apr 2026 13:38:00 +0200 Subject: [PATCH] fix(ios-chat): dismissable keyboard via swipe + toolbar button (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scarf/Scarf iOS/Chat/ChatView.swift | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scarf/Scarf iOS/Chat/ChatView.swift b/scarf/Scarf iOS/Chat/ChatView.swift index f6561a8..d1de4da 100644 --- a/scarf/Scarf iOS/Chat/ChatView.swift +++ b/scarf/Scarf iOS/Chat/ChatView.swift @@ -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() }