From c1fd2b9a641c6fed8cb455859a7ba8ac54c0739e Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 26 Nov 2024 10:37:16 +0100 Subject: [PATCH] fix: no random onStart/onEnd events during interactive keyboard dismissal --- TODO | 7 ++-- .../InvisibleInputAccessoryView.swift | 9 +++-- ios/interactive/KeyboardAreaExtender.swift | 9 ++++- ios/observers/KeyboardEventsIgnorer.swift | 14 ++++++-- ios/observers/KeyboardMovementObserver.swift | 20 +++++------ ios/swizzling/UIResponderSwizzle.swift | 7 ++-- ios/views/KeyboardGestureAreaManager.mm | 36 +++++++++++++++++-- 7 files changed, 79 insertions(+), 23 deletions(-) diff --git a/TODO b/TODO index 2207d1c228..457f8d459b 100644 --- a/TODO +++ b/TODO @@ -4,9 +4,10 @@ - (x) text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) - maybe because of frequent conversions to CGFloat? <- fixed via rounding - 1 show after interactive - keyboard height is 386 - 2 show after interactive - iav is not attached again -- 3 two events get dispatched when we remove iav in onInteractive - (dispatch `shouldIgnoreKeyboardEvents` in .hide (KEA)? - doesn't work because swizzle calls .hide when keyboard appear) +- (x) 3 two events get dispatched when we remove iav in onInteractive - (dispatch `shouldIgnoreKeyboardEvents` in .hide (KEA)? - doesn't work because swizzle calls .hide when keyboard appear) +- (x) 4 two events get dispatched after 2nd onInteractive (fix with pan gesture responder - when it's active ignore plain keyboard events) -:: call actual code for hide only once -::: dispatch ignore event when we actually hide iav (will fix 3) +:: (x) call actual code for hide only once +::: (x) dispatch ignore event when we actually hide iav (will fix 3) ::: (add `lastOffset`) - will fix 1 <- last time when I added it was resetting somehow, need to figure out where and why exactly ::: add code in `keyboardDidAppear` in KAE to increase/attach iav again (will fix 2) \ No newline at end of file diff --git a/ios/interactive/InvisibleInputAccessoryView.swift b/ios/interactive/InvisibleInputAccessoryView.swift index 5ed8387306..c9e6ab04e3 100644 --- a/ios/interactive/InvisibleInputAccessoryView.swift +++ b/ios/interactive/InvisibleInputAccessoryView.swift @@ -9,6 +9,8 @@ import Foundation import UIKit public class InvisibleInputAccessoryView: UIView { + var isShown = true + override init(frame: CGRect) { super.init(frame: frame) setupView() @@ -37,6 +39,9 @@ public class InvisibleInputAccessoryView: UIView { } public func hide() { + guard isShown else { return } + isShown = false + print("hide") updateHeight(to: 0.0) superview?.layoutIfNeeded() } @@ -48,8 +53,8 @@ public class InvisibleInputAccessoryView: UIView { private func setupView() { isUserInteractionEnabled = false // TODO: Set the background color to transparent - // backgroundColor = UIColor.red - backgroundColor = .clear + backgroundColor = UIColor.red + // backgroundColor = .clear autoresizingMask = .flexibleHeight } } diff --git a/ios/interactive/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift index 5433e90fd2..e380046094 100644 --- a/ios/interactive/KeyboardAreaExtender.swift +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -29,7 +29,14 @@ class KeyboardAreaExtender: NSObject { } public func hide() { - currentInputAccessoryView?.hide() + if (currentInputAccessoryView?.isShown ?? false) { + print("hide iav") + NotificationCenter.default.post( + name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true] + ) + currentInputAccessoryView?.hide() + } + print("ignore hide") } public func updateHeight(_ to: CGFloat, _ nativeID: String) { diff --git a/ios/observers/KeyboardEventsIgnorer.swift b/ios/observers/KeyboardEventsIgnorer.swift index 6445c48c8e..9ad684f829 100644 --- a/ios/observers/KeyboardEventsIgnorer.swift +++ b/ios/observers/KeyboardEventsIgnorer.swift @@ -7,10 +7,20 @@ import Foundation -class KeyboardEventsIgnorer { +@objc(KeyboardEventsIgnorer) +public class KeyboardEventsIgnorer : NSObject { + @objc public static let shared = KeyboardEventsIgnorer() + var shouldIgnoreKeyboardEvents = false + @objc public var isInteractiveGesture = false + + public var shouldIgnore : Bool { + print("KeyboardEventsIgnorer \(shouldIgnoreKeyboardEvents) \(isInteractiveGesture)") + return shouldIgnoreKeyboardEvents || isInteractiveGesture + } - init() { + override init() { + super.init() NotificationCenter.default.addObserver( self, selector: #selector(handleIgnoreKeyboardEventsNotification), diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 4b6f372228..2e044e8de4 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -51,8 +51,6 @@ public class KeyboardMovementObserver: NSObject { private var tag: NSNumber = -1 private var animation: KeyboardAnimation? private var didShowDeadline: Int64 = 0 - // external class instances - private let eventsIgnorer = KeyboardEventsIgnorer() @objc public init( handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void, @@ -160,9 +158,7 @@ public class KeyboardMovementObserver: NSObject { } prevKeyboardPosition = position - // TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? - KeyboardAreaExtender.shared.hide() - /// + onEvent( "onKeyboardMoveInteractive", position as NSNumber, @@ -170,6 +166,10 @@ public class KeyboardMovementObserver: NSObject { -1, tag ) + + // TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? + KeyboardAreaExtender.shared.hide() + /// } } @@ -180,7 +180,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardWillAppear(_ notification: Notification) { - guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { return } + guard !KeyboardEventsIgnorer.shared.shouldIgnore else { return } print("keyboardWillAppear \(Date.currentTimeStamp)") let (duration, frame) = notification.keyboardMetaData() if let keyboardFrame = frame { @@ -203,7 +203,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardWillDisappear(_ notification: Notification) { - guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { return } + guard !KeyboardEventsIgnorer.shared.shouldIgnore else { return } print("keyboardWillDisappear \(Date.currentTimeStamp)") let (duration, _) = notification.keyboardMetaData() tag = UIResponder.current.reactViewTag @@ -228,8 +228,8 @@ public class KeyboardMovementObserver: NSObject { tag = UIResponder.current.reactViewTag self.keyboardHeight = keyboardHeight - guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { - eventsIgnorer.shouldIgnoreKeyboardEvents = false + guard !KeyboardEventsIgnorer.shared.shouldIgnore else { + KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false return } @@ -255,7 +255,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardDidDisappear(_ notification: Notification) { - guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { return } + guard !KeyboardEventsIgnorer.shared.shouldIgnore else { return } print("keyboardDidDisappear \(Date.currentTimeStamp)") let (duration, _) = notification.keyboardMetaData() tag = UIResponder.current.reactViewTag diff --git a/ios/swizzling/UIResponderSwizzle.swift b/ios/swizzling/UIResponderSwizzle.swift index 735b3b136c..c2668171e6 100644 --- a/ios/swizzling/UIResponderSwizzle.swift +++ b/ios/swizzling/UIResponderSwizzle.swift @@ -30,9 +30,10 @@ extension UIResponder { // Add your custom behavior here print("Performing custom actions before the original resignFirstResponder") - if let textField = self as? TextInput { - (textField.inputAccessoryView as? InvisibleInputAccessoryView)?.hide() - } + KeyboardAreaExtender.shared.hide() + // if let textField = self as? TextInput { + // (textField.inputAccessoryView as? InvisibleInputAccessoryView)?.hide() + // } // Postpone execution of the original resignFirstResponder DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) { diff --git a/ios/views/KeyboardGestureAreaManager.mm b/ios/views/KeyboardGestureAreaManager.mm index b1d877ffa8..e6e8716370 100644 --- a/ios/views/KeyboardGestureAreaManager.mm +++ b/ios/views/KeyboardGestureAreaManager.mm @@ -53,11 +53,14 @@ - (UIView *)view // MARK: View #ifdef RCT_NEW_ARCH_ENABLED -@interface KeyboardGestureArea () -@end +@interface KeyboardGestureArea () +#else +@interface KeyboardGestureArea () #endif +@end @implementation KeyboardGestureArea { + UIPanGestureRecognizer *_panGestureRecognizer; } #ifdef RCT_NEW_ARCH_ENABLED @@ -81,6 +84,7 @@ + (void)load - (instancetype)init { if (self = [super init]) { + [self setupGestureRecognizers]; } return self; } @@ -89,6 +93,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { self = [super initWithFrame:CGRectZero]; if (self) { + [self setupGestureRecognizers]; } return self; @@ -100,6 +105,33 @@ - (void)dealloc [[KeyboardOffsetProvider shared] removeOffsetForTextInputNativeID:_textInputNativeID]; } +// MARK: Gesture Recognizers +- (void)setupGestureRecognizers +{ + _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; + _panGestureRecognizer.delegate = self; // Set delegate to enable simultaneous recognition + [self addGestureRecognizer:_panGestureRecognizer]; +} + +- (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { + [KeyboardEventsIgnorer shared].isInteractiveGesture = YES; + } + if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { + NSLog(@"set to false"); + [KeyboardEventsIgnorer shared].isInteractiveGesture = NO; + } + +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer + shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + // Allow simultaneous gesture recognition + return YES; +} + // MARK: lifecycle methods - (void)didMoveToSuperview {