Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
let onSendCash: () -> Void
let conversationController: ConversationController
let barModel: ConversationBarModel
/// Focus the composer when the bar first appears (post-tip open). One-shot:
/// the composer requests focus in its `.task`, which runs once on appear.
/// Raise the keyboard when the screen first appears (post-tip open). The UIKit screen focuses
/// the composer field in `viewDidAppear` — a hosted SwiftUI `@FocusState` never presents the
/// keyboard across the hosting boundary.
let focusOnAppear: Bool
/// Whether this is a tip DM — the send button then stays minimized.
let isTipDm: Bool
Expand All @@ -49,6 +50,7 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
let barHost = UIHostingController(rootView: bar(coordinator: context.coordinator))
barHost.view.backgroundColor = .clear
let screen = ChatScreenViewController(bar: barHost.view, barController: barHost)
screen.focusesComposerOnAppear = focusOnAppear
screen.onReachTop = onReachTop
screen.onRetry = onRetry
screen.onCashCardTap = onCashCardTap
Expand Down Expand Up @@ -102,7 +104,6 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
symbol: symbol,
onSendCash: onSendCash,
model: barModel,
focusOnAppear: focusOnAppear,
isTipDm: isTipDm
)
.environment(conversationController)
Expand Down
21 changes: 2 additions & 19 deletions Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ struct ConversationBottomBar: View {
let symbol: String
let onSendCash: () -> Void
let model: ConversationBarModel
/// Focus the composer on first appear (post-tip open); false everywhere else.
var focusOnAppear: Bool = false
/// Tip chats always show the compact symbol-only send button; ordinary
/// chats expand to "Send €" at rest and collapse only while composing.
var isTipDm: Bool = false
Expand All @@ -64,12 +62,8 @@ struct ConversationBottomBar: View {
)
}
if chatExists {
ConversationComposer(
conversationID: conversationID,
model: model,
focusOnAppear: focusOnAppear
)
.transition(.opacity)
ConversationComposer(conversationID: conversationID, model: model)
.transition(.opacity)
}
}
.padding(.horizontal, 12)
Expand All @@ -90,8 +84,6 @@ struct ConversationComposer: View {

let conversationID: ConversationID?
@Bindable var model: ConversationBarModel
/// Raise the keyboard on open (post-tip chats only). One-shot via `.task`.
var focusOnAppear: Bool = false

@Environment(ConversationController.self) private var conversationController
@FocusState private var isFocused: Bool
Expand Down Expand Up @@ -133,15 +125,6 @@ struct ConversationComposer: View {

return field
.glassBackground(cornerRadius: BarMetrics.cornerRadius)
// Post-tip open: raise the keyboard once the composer appears. Requested
// after a short delay so the push transition has settled — focus asked
// mid-transition is dropped and the keyboard never rises. `.task` runs
// once on appear and is cancelled on disappear.
.task {
guard focusOnAppear else { return }
try? await Task.delay(milliseconds: 350)
isFocused = true
}
// Focus is the single source of `isComposing` — the button morph and the
// screen's interactive-dismiss gate both key off it. Losing focus
// (keyboard swiped down) ends composing.
Expand Down
30 changes: 30 additions & 0 deletions FlipcashUI/Sources/FlipcashUI/Chat/ChatScreenViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public final class ChatScreenViewController: UIViewController {
/// lets the composer overflow below its frame, under the keyboard.
private var barHeightConstraint: NSLayoutConstraint!

/// Raise the keyboard once the screen has finished appearing (post-tip open). Driven from
/// UIKit rather than a SwiftUI `@FocusState`: a hosted composer's programmatic focus updates
/// SwiftUI's focus state but never presents the keyboard across the hosting boundary — only a
/// real `becomeFirstResponder` does.
public var focusesComposerOnAppear = false
private var didFocusComposer = false

/// - Parameters:
/// - bar: pinned to the keyboard layout guide; rides the keyboard.
/// - barController: the view controller owning the bar, when hosted (e.g. a
Expand Down Expand Up @@ -119,6 +126,17 @@ public final class ChatScreenViewController: UIViewController {
host.setContentScrollView(transcript.collectionView, for: .top)
}

public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Raise the keyboard once, after the push transition settles — the composer's field is now
// in the key window, so `becomeFirstResponder` presents the keyboard (a hosted SwiftUI
// `@FocusState` set programmatically does not). One-shot: guarded so a later re-appear
// (app foregrounding) doesn't force the keyboard back up.
guard focusesComposerOnAppear, !didFocusComposer else { return }
didFocusComposer = true
bar.firstTextInputResponder?.becomeFirstResponder()
}

/// Set the bar's height to its measured SwiftUI content height.
public func setBarHeight(_ height: CGFloat) {
guard barHeightConstraint != nil, barHeightConstraint.constant != height else { return }
Expand All @@ -140,4 +158,16 @@ public final class ChatScreenViewController: UIViewController {
transcript.setBottomInset(bar.frame.height)
}
}

private extension UIView {
/// The first descendant text-input view that can become first responder — the composer's
/// field, wherever SwiftUI nests it inside the hosted bar.
var firstTextInputResponder: UIView? {
if (self is UITextField || self is UITextView), canBecomeFirstResponder { return self }
for subview in subviews {
if let responder = subview.firstTextInputResponder { return responder }
}
return nil
}
}
#endif