Fix keyboard swipe-back handling

Keep the keyboard attached during interactive navigation pops by skipping the normal ChatController editing dismissal while the controller is being interactively popped.

Cancel interactive keyboard gestures when a horizontal pop begins, so vertical jitter in the back-swipe gesture does not trigger keyboard-dismiss tracking during navigation.

Force the delayed keyboard hide layout update after programmatic dismissal to use an immediate transition, avoiding visible movement after returning to the chat list.
This commit is contained in:
Vladislav Annenkov 2026-06-27 20:27:22 +02:00
parent 6e370e06d1
commit 3306d4cf24
5 changed files with 46 additions and 19 deletions

View file

@ -127,13 +127,6 @@ class KeyboardManager {
}
}
private func endAnimations(view: UIView) {
view.layer.removeAllAnimations()
for subview in view.subviews {
endAnimations(view: subview)
}
}
public func viewTreeContainsFirstResponder(view: UIView) -> Bool {
if view.isFirstResponder {
return true
@ -149,6 +142,7 @@ public func viewTreeContainsFirstResponder(view: UIView) -> Bool {
public final class KeyboardViewManager {
private let host: StatusBarHost
var willDismissEditingWithoutAnimation: (() -> Void)?
init(host: StatusBarHost) {
self.host = host
@ -156,11 +150,9 @@ public final class KeyboardViewManager {
public func dismissEditingWithoutAnimation(view: UIView) {
if viewTreeContainsFirstResponder(view: view) {
view.endEditing(true)
if let keyboardWindow = self.host.keyboardWindow {
for view in keyboardWindow.subviews {
endAnimations(view: view)
}
self.willDismissEditingWithoutAnimation?()
UIView.performWithoutAnimation {
view.endEditing(true)
}
}
}

View file

@ -233,6 +233,8 @@ public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelega
return
}
topController.cancelInteractiveKeyboardGestures()
topController.isBeingInteractivelyPopped = true
topController.viewWillDisappear(true)
let topNode = topController.displayNode
var bottomControllerLayout = layout
@ -299,6 +301,7 @@ public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelega
strongSelf.state.transition = nil
strongSelf.controllerRemoved(top.value)
topController.isBeingInteractivelyPopped = false
strongSelf.ignoreInputHeight = false
})
} else {
@ -307,7 +310,8 @@ public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelega
return
}
strongSelf.state.transition = nil
top.value.isBeingInteractivelyPopped = false
top.value.viewDidAppear(true)
transition.previous.value.viewDidDisappear(true)
})

View file

@ -234,6 +234,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
private weak var activeInputView: UIResponder?
open var hasActiveInput: Bool = false
public internal(set) var isBeingInteractivelyPopped: Bool = false
open var overlayWantsToBeBelowKeyboard: Bool {
return false
@ -657,6 +658,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
open func viewWillLeaveNavigation() {
}
open func cancelInteractiveKeyboardGestures() {
self.view.windowHost?.cancelInteractiveKeyboardGestures()
}
open override func viewDidAppear(_ animated: Bool) {
self.activeInputView = nil

View file

@ -293,6 +293,7 @@ public class Window1 {
private var statusBarHidden = false
private var shouldNotAnimateLikelyKeyboardAutocorrectionSwitch: Bool = false
private var suppressNextKeyboardHideAnimationUntil: Double?
public private(set) var forceInCallStatusBarText: String? = nil
public var inCallNavigate: (() -> Void)?
@ -477,6 +478,10 @@ public class Window1 {
self.topPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate)
self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate)
self.keyboardViewManager?.willDismissEditingWithoutAnimation = { [weak self] in
self?.suppressNextKeyboardHideAnimationUntil = CACurrentMediaTime() + 1.0
}
//TODO:release check old iOS
/*self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: UIApplication.willChangeStatusBarFrameNotification, object: nil, queue: OperationQueue.main, using: { [weak self] notification in
if let strongSelf = self, strongSelf.statusBarHost != nil {
@ -648,6 +653,9 @@ public class Window1 {
transition = .immediate
}
}
if strongSelf.shouldSuppressNextKeyboardHideAnimation(keyboardHeight: keyboardHeight) {
transition = .immediate
}
strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: transition, overrideTransition: false) }
}
@ -717,6 +725,21 @@ public class Window1 {
self.hostView.containerView.addGestureRecognizer(recognizer)
self.hostView.containerView.addSubview(self.badgeView)
}
private func shouldSuppressNextKeyboardHideAnimation(keyboardHeight: CGFloat) -> Bool {
guard let suppressNextKeyboardHideAnimationUntil = self.suppressNextKeyboardHideAnimationUntil else {
return false
}
if suppressNextKeyboardHideAnimationUntil < CACurrentMediaTime() {
self.suppressNextKeyboardHideAnimationUntil = nil
return false
}
if keyboardHeight.isLessThanOrEqualTo(0.0) {
self.suppressNextKeyboardHideAnimationUntil = nil
return true
}
return false
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")

View file

@ -7967,12 +7967,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if #available(iOS 18.0, *) {
} else {
//TODO:release
}
UIView.performWithoutAnimation {
self.view.endEditing(true)
if !self.isBeingInteractivelyPopped {
UIView.performWithoutAnimation {
self.view.endEditing(true)
}
}
self.chatDisplayNode.historyNode.canReadHistory.set(.single(false))
@ -8048,6 +8046,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.chatDisplayNode.willNavigateAway()
}
override public func cancelInteractiveKeyboardGestures() {
super.cancelInteractiveKeyboardGestures()
self.chatDisplayNode.cancelInteractiveKeyboardGestures()
}
override public func inFocusUpdated(isInFocus: Bool) {
self.disableStickerAnimationsPromise.set(!isInFocus)
self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus)