From efd6980f2908c97d107b8f72bb9922e00bbcc31f Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 2 Nov 2024 17:34:36 +0100 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20add=20basic=20fs=20structure=20fo?= =?UTF-8?q?r=20managing=20keyboard=20offset=20(just=20save=20changes=20bef?= =?UTF-8?q?ore=20macOS=2014=20->=20macOS=2015=20update)=F0=9F=A4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Examples/InteractiveKeyboardIOS/index.tsx | 32 +++- ios/extensions/UIResponder.swift | 7 + .../InvisibleInputAccessoryView.swift | 50 ++++++ ios/interactive/KeyboardAreaExtender.swift | 30 ++++ ios/interactive/KeyboardOffsetProvider.swift | 35 +++++ ios/observers/KeyboardMovementObserver.swift | 18 ++- ios/protocols/TextInput.swift | 12 +- ios/swizzling/UIResponderSwizzle.swift | 48 ++++++ ios/views/KeyboardGestureAreaManager.h | 31 ++++ ios/views/KeyboardGestureAreaManager.mm | 147 ++++++++++++++++++ src/bindings.native.ts | 2 +- .../KeyboardGestureAreaNativeComponent.ts | 7 +- src/types.ts | 6 +- 13 files changed, 407 insertions(+), 18 deletions(-) create mode 100644 ios/interactive/InvisibleInputAccessoryView.swift create mode 100644 ios/interactive/KeyboardAreaExtender.swift create mode 100644 ios/interactive/KeyboardOffsetProvider.swift create mode 100644 ios/swizzling/UIResponderSwizzle.swift create mode 100644 ios/views/KeyboardGestureAreaManager.h create mode 100644 ios/views/KeyboardGestureAreaManager.mm diff --git a/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx b/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx index 4d989e41b7..841d39a96b 100644 --- a/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx +++ b/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx @@ -1,6 +1,9 @@ -import React, { useCallback, useRef } from "react"; +import React, { useCallback, useRef, useState } from "react"; import { TextInput, View } from "react-native"; -import { useKeyboardHandler } from "react-native-keyboard-controller"; +import { + KeyboardGestureArea, + useKeyboardHandler, +} from "react-native-keyboard-controller"; import Reanimated, { useAnimatedProps, useAnimatedScrollHandler, @@ -13,6 +16,8 @@ import { history } from "../../../components/Message/data"; import styles from "./styles"; +import type { LayoutChangeEvent } from "react-native"; + const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput); const useKeyboardAnimation = () => { @@ -86,6 +91,11 @@ const contentContainerStyle = { function InteractiveKeyboard() { const ref = useRef(null); const { height, onScroll, inset, offset } = useKeyboardAnimation(); + const [inputHeight, setInputHeight] = useState(TEXT_INPUT_HEIGHT); + + const onInputLayoutChanged = useCallback((e: LayoutChangeEvent) => { + setInputHeight(e.nativeEvent.layout.height); + }, []); const scrollToBottom = useCallback(() => { ref.current?.scrollToEnd({ animated: false }); @@ -94,7 +104,7 @@ function InteractiveKeyboard() { const textInputStyle = useAnimatedStyle( () => ({ position: "absolute", - height: TEXT_INPUT_HEIGHT, + minHeight: TEXT_INPUT_HEIGHT, width: "100%", backgroundColor: "#BCBCBC", transform: [{ translateY: -height.value }], @@ -113,7 +123,11 @@ function InteractiveKeyboard() { })); return ( - + ))} - - + + ); } diff --git a/ios/extensions/UIResponder.swift b/ios/extensions/UIResponder.swift index b3fdf17b7f..6d3fa854c6 100644 --- a/ios/extensions/UIResponder.swift +++ b/ios/extensions/UIResponder.swift @@ -44,6 +44,13 @@ public extension Optional where Wrapped == UIResponder { return (self as? UIView)?.superview?.reactTag ?? -1 #endif } + + var nativeID: String? { + if let superview = (self as? UIView)?.superview as? UIView { + return superview.nativeID as String? + } + return nil + } } public extension Optional where Wrapped: UIResponder { diff --git a/ios/interactive/InvisibleInputAccessoryView.swift b/ios/interactive/InvisibleInputAccessoryView.swift new file mode 100644 index 0000000000..58cd37defc --- /dev/null +++ b/ios/interactive/InvisibleInputAccessoryView.swift @@ -0,0 +1,50 @@ +// +// InvisibleInputAccessoryView.swift +// Pods +// +// Created by Kiryl Ziusko on 01/11/2024. +// + +import Foundation +import UIKit + +public class InvisibleInputAccessoryView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + public convenience init(height: CGFloat) { + self.init(frame: CGRect.init(x: 0, y: 0, width: 0, height: height)) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + // Return false to allow touch events to pass through + return false + } + + public func updateHeight(to newHeight: CGFloat) { + self.frame = CGRect.init(x: 0, y: 0, width: 0, height: newHeight) + + // Invalidate intrinsic content size to trigger a layout update + invalidateIntrinsicContentSize() + self.layoutIfNeeded() + } + + public override var intrinsicContentSize: CGSize { + return CGSize(width: UIView.noIntrinsicMetric, height: self.frame.height) + } + + private func setupView() { + isUserInteractionEnabled = false + // TODO: Set the background color to transparent + backgroundColor = UIColor.red + // self.backgroundColor = .clear + autoresizingMask = .flexibleHeight + } +} diff --git a/ios/interactive/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift new file mode 100644 index 0000000000..a13efa5565 --- /dev/null +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -0,0 +1,30 @@ +// +// KeyboardAreaExtender.swift +// Pods +// +// Created by Kiryl Ziusko on 02/11/2024. +// + +class KeyboardAreaExtender : NSObject { + private var currentInputAccessoryView: InvisibleInputAccessoryView? + + @objc public static let shared = KeyboardAreaExtender() + + private override init() { + + } + + public var offset: Int { + get { + return currentInputAccessoryView?.frame?.height ?? 0 + } + } + + public func remove() { + + } + + public func updateHeight(newHeight to: CGFloat) { + currentInputAccessoryView?.updateHeight(to: newHeight) + } +} diff --git a/ios/interactive/KeyboardOffsetProvider.swift b/ios/interactive/KeyboardOffsetProvider.swift new file mode 100644 index 0000000000..88a91556a7 --- /dev/null +++ b/ios/interactive/KeyboardOffsetProvider.swift @@ -0,0 +1,35 @@ +// +// KeyboardOffsetProvider.swift +// Pods +// +// Created by Kiryl Ziusko on 01/11/2024. +// + +import Foundation + +@objc(KeyboardOffsetProvider) +public class KeyboardOffsetProvider: NSObject { + @objc public static let shared = KeyboardOffsetProvider() + + private override init() {} + + private var offsetMap: [String: NSNumber] = [:] + + @objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber) { + offsetMap[nativeID] = offset + } + + @objc public func getOffset(forTextInputNativeID nativeID: String?) -> NSNumber? { + guard let unwrappedNativeID = nativeID else { return nil } + return offsetMap[unwrappedNativeID] + } + + @objc public func hasOffset(forTextInputNativeID nativeID: String?) -> Bool { + guard let unwrappedNativeID = nativeID else { return false } + return offsetMap[unwrappedNativeID] != nil + } + + @objc public func removeOffset(forTextInputNativeID nativeID: String) { + offsetMap.removeValue(forKey: nativeID) + } +} diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 68871b15a5..87773f3060 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -41,6 +41,10 @@ public class KeyboardMovementObserver: NSObject { private var tag: NSNumber = -1 private var animation: KeyboardAnimation? private var didShowDeadline: Int64 = 0 + // TODO: should we move all interactive stuff into separate file? + // Or just move new functionality there? + // interactive keyboard + private var inputAccessoryView: UIView? @objc public init( handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void, @@ -200,7 +204,8 @@ public class KeyboardMovementObserver: NSObject { if let keyboardFrame = frame { let (position, _) = keyboardView.frameTransitionInWindow let keyboardHeight = keyboardFrame.cgRectValue.size.height - tag = UIResponder.current.reactViewTag + let responder = UIResponder.current + tag = responder.reactViewTag self.keyboardHeight = keyboardHeight // if the event is caught in between it's highly likely that it could be a "resize" event // so we just read actual keyboard frame value in this case @@ -215,6 +220,17 @@ public class KeyboardMovementObserver: NSObject { removeKeyboardWatcher() setupKVObserver() animation = nil + + if let activeTextInput = responder as? TextInput, + let offset = KeyboardOffsetProvider.shared.getOffset( + forTextInputNativeID: responder.nativeID), + responder?.inputAccessoryView == nil + { + inputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(offset)) + + activeTextInput.inputAccessoryView = inputAccessoryView + activeTextInput.reloadInputViews() + } } } diff --git a/ios/protocols/TextInput.swift b/ios/protocols/TextInput.swift index deb1458453..be7707667b 100644 --- a/ios/protocols/TextInput.swift +++ b/ios/protocols/TextInput.swift @@ -1,15 +1,15 @@ // // TextInput.swift -// KeyboardController +// Pods // -// Created by Kiryl Ziusko on 27/01/2024. -// Copyright © 2024 Facebook. All rights reserved. +// Created by Kiryl Ziusko on 01/11/2024. // -import Foundation -import UIKit - public protocol TextInput: AnyObject { + // default common methods/properties + var inputAccessoryView: UIView? { get set } + func reloadInputViews() + // custom methods/properties func focus() } diff --git a/ios/swizzling/UIResponderSwizzle.swift b/ios/swizzling/UIResponderSwizzle.swift new file mode 100644 index 0000000000..41e4ee6728 --- /dev/null +++ b/ios/swizzling/UIResponderSwizzle.swift @@ -0,0 +1,48 @@ +// +// UIResponder.swift +// Pods +// +// Created by Kiryl Ziusko on 01/11/2024. +// + +import Foundation +import UIKit + +private var originalResignFirstResponder: IMP? + +@objc +extension UIResponder { + public static func swizzleResignFirstResponder() { + let originalSelector = #selector(resignFirstResponder) + + guard let originalMethod = class_getInstanceMethod(UIResponder.self, originalSelector) else { + return + } + + originalResignFirstResponder = method_getImplementation(originalMethod) + + let swizzledImplementation: @convention(block) (UIResponder) -> Bool = { (self) in + // Add your custom behavior here + print("Performing custom actions before the original resignFirstResponder") + + if let textField = self as? TextInput { + (textField.inputAccessoryView as? InvisibleInputAccessoryView)?.updateHeight(to: 0) + textField.inputAccessoryView?.superview?.layoutIfNeeded() + } + + // Postpone execution of the original resignFirstResponder + DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) { + // Call the original resignFirstResponder + typealias Function = @convention(c) (AnyObject, Selector) -> Bool + let castOriginalResignFirstResponder = unsafeBitCast( + originalResignFirstResponder, to: Function.self) + _ = castOriginalResignFirstResponder(self, originalSelector) + } + + return true // We need to return a value immediately, even though the actual action is delayed + } + + let implementation = imp_implementationWithBlock(swizzledImplementation) + method_setImplementation(originalMethod, implementation) + } +} diff --git a/ios/views/KeyboardGestureAreaManager.h b/ios/views/KeyboardGestureAreaManager.h new file mode 100644 index 0000000000..63b98199e8 --- /dev/null +++ b/ios/views/KeyboardGestureAreaManager.h @@ -0,0 +1,31 @@ +// +// KeyboardGestureAreaManager.h +// Pods +// +// Created by Kiryl Ziusko on 01/11/2024. +// + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#else +#import +#endif +#import +#import + +@interface KeyboardGestureAreaManager : RCTViewManager +@end + +@interface KeyboardGestureArea : +#ifdef RCT_NEW_ARCH_ENABLED + RCTViewComponentView +#else + UIView + +- (instancetype)initWithBridge:(RCTBridge *)bridge; + +#endif +// TODO: should be array? How to specify multiple different offsets for different inputs? +@property (nonatomic, assign) NSNumber* offset; +@property (nonatomic, assign) NSString* textInputNativeID; +@end diff --git a/ios/views/KeyboardGestureAreaManager.mm b/ios/views/KeyboardGestureAreaManager.mm new file mode 100644 index 0000000000..06754e7f94 --- /dev/null +++ b/ios/views/KeyboardGestureAreaManager.mm @@ -0,0 +1,147 @@ +// +// KeyboardGestureAreaManager.mm +// Pods +// +// Created by Kiryl Ziusko on 01/11/2024. +// + +#import "KeyboardGestureAreaManager.h" + +#if __has_include("react_native_keyboard_controller-Swift.h") +#import "react_native_keyboard_controller-Swift.h" +#else +#import +#endif + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import + +#import "RCTFabricComponentsPlugins.h" +#endif + +#import + +#ifdef RCT_NEW_ARCH_ENABLED +using namespace facebook::react; +#endif + +// MARK: Manager +@implementation KeyboardGestureAreaManager + +RCT_EXPORT_MODULE(KeyboardGestureAreaManager) + +// Expose props to React Native +RCT_EXPORT_VIEW_PROPERTY(textInputNativeID, NSString*) +RCT_EXPORT_VIEW_PROPERTY(offset, NSNumber*) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +#ifndef RCT_NEW_ARCH_ENABLED +- (UIView *)view +{ + return [[KeyboardGestureArea alloc] initWithBridge:self.bridge]; +} +#endif + +@end + +// MARK: View +#ifdef RCT_NEW_ARCH_ENABLED +@interface KeyboardGestureArea () +@end +#endif + +@implementation KeyboardGestureArea { +} + +#ifdef RCT_NEW_ARCH_ENABLED ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} +#endif + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load +{ + [super load]; + + // TODO: initialize swizzling here? Or in constructor? + [UIResponder swizzleResignFirstResponder]; +} + +// MARK: Constructor +#ifdef RCT_NEW_ARCH_ENABLED +- (instancetype)init +{ + if (self = [super init]) { + + } + return self; +} +#else +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + self = [super initWithFrame:CGRectZero]; + if (self) { + + } + + return self; +} +#endif + +- (void)dealloc +{ + [[KeyboardOffsetProvider shared] removeOffsetForTextInputNativeID:_textInputNativeID]; +} + +// MARK: lifecycle methods +- (void)didMoveToSuperview +{ + if (self.superview == nil) { + // unmounted + } +} + +// MARK: props updater +#ifdef RCT_NEW_ARCH_ENABLED +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &oldViewProps = *std::static_pointer_cast(_props); + const auto &newViewProps = *std::static_pointer_cast(props); + + // TODO: implement logic with provider + + [super updateProps:props oldProps:oldProps]; +} +#else +- (void)setOffset:(NSNumber*)offset +{ + // [[KeyboardOffsetProvider shared] removeOffsetForTextInputNativeID:_textInputNativeID]; + [[KeyboardOffsetProvider shared] setOffsetForTextInputNativeID:_textInputNativeID offset:offset]; + _offset = offset; +} + +- (void)setTextInputNativeID:(NSString*)textInputNativeID +{ + [[KeyboardOffsetProvider shared] removeOffsetForTextInputNativeID:_textInputNativeID]; + [[KeyboardOffsetProvider shared] setOffsetForTextInputNativeID:textInputNativeID offset:_offset]; + _textInputNativeID = textInputNativeID; +} +#endif + +#ifdef RCT_NEW_ARCH_ENABLED +Class KeyboardGestureAreaCls(void) +{ + return KeyboardGestureArea.class; +} +#endif + +@end diff --git a/src/bindings.native.ts b/src/bindings.native.ts index c1ea9dcd76..d1621d1838 100644 --- a/src/bindings.native.ts +++ b/src/bindings.native.ts @@ -55,7 +55,7 @@ export const WindowDimensionsEvents: WindowDimensionsEventsModule = { export const KeyboardControllerView: React.FC = require("./specs/KeyboardControllerViewNativeComponent").default; export const KeyboardGestureArea: React.FC = - Platform.OS === "android" && Platform.Version >= 30 + (Platform.OS === "android" && Platform.Version >= 30) || Platform.OS === "ios" ? require("./specs/KeyboardGestureAreaNativeComponent").default : ({ children }: KeyboardGestureAreaProps) => children; export const RCTOverKeyboardView: React.FC = diff --git a/src/specs/KeyboardGestureAreaNativeComponent.ts b/src/specs/KeyboardGestureAreaNativeComponent.ts index d28e492ecd..58fa63863d 100644 --- a/src/specs/KeyboardGestureAreaNativeComponent.ts +++ b/src/specs/KeyboardGestureAreaNativeComponent.ts @@ -12,8 +12,9 @@ export interface NativeProps extends ViewProps { showOnSwipeUp?: boolean; enableSwipeToDismiss?: boolean; offset?: Double; + textInputNativeID?: string; } -export default codegenNativeComponent("KeyboardGestureArea", { - excludedPlatforms: ["iOS"], -}) as HostComponent; +export default codegenNativeComponent( + "KeyboardGestureArea", +) as HostComponent; diff --git a/src/types.ts b/src/types.ts index b2801519cf..a9661d7d6a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,7 +92,7 @@ export type KeyboardControllerProps = { } & ViewProps; export type KeyboardGestureAreaProps = { - interpolator: "ios" | "linear"; + interpolator?: "ios" | "linear"; /** * Whether to allow to show a keyboard from dismissed state by swipe up. * Default to `false`. @@ -108,6 +108,10 @@ export type KeyboardGestureAreaProps = { * Extra distance to the keyboard. */ offset?: number; + /** + * A corresponding `nativeID` value from the corresponding `TextInput`. + */ + textInputNativeID?: string; } & ViewProps; export type OverKeyboardViewProps = PropsWithChildren<{ visible: boolean; From 81d98466ec0c7bb7ba3d867066ef091a6ef279ff Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sun, 3 Nov 2024 01:01:57 +0100 Subject: [PATCH 02/22] feat: more changes to actually move progress further --- ios/interactive/KeyboardAreaExtender.swift | 41 +++++++++++++--- ios/interactive/KeyboardOffsetProvider.swift | 1 + ios/observers/KeyboardMovementObserver.swift | 51 +++++++++----------- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/ios/interactive/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift index a13efa5565..5c114ab2d0 100644 --- a/ios/interactive/KeyboardAreaExtender.swift +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -11,20 +11,47 @@ class KeyboardAreaExtender : NSObject { @objc public static let shared = KeyboardAreaExtender() private override init() { - + super.init() + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardDidAppear), + name: UIResponder.keyboardDidShowNotification, + object: nil + ) } - public var offset: Int { + deinit { + NotificationCenter.default.removeObserver(self) + } + + public var offset: CGFloat { get { - return currentInputAccessoryView?.frame?.height ?? 0 + return currentInputAccessoryView?.frame.height ?? 0 } } - public func remove() { - + public func hide() { + // currentInputAccessoryView?.updateHeight(to: 0.0) + // superview?.layoutIfNeeded() } - public func updateHeight(newHeight to: CGFloat) { - currentInputAccessoryView?.updateHeight(to: newHeight) + public func updateHeight(_ to: CGFloat, _ nativeID: String) { + if (UIResponder.current.nativeID == nativeID) { + currentInputAccessoryView?.updateHeight(to: to) + } + } + + @objc private func keyboardDidAppear(_ notification: Notification) { + let responder = UIResponder.current + if let activeTextInput = responder as? TextInput, + let offset = KeyboardOffsetProvider.shared.getOffset( + forTextInputNativeID: responder.nativeID), + responder?.inputAccessoryView == nil + { + currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(offset)) + + activeTextInput.inputAccessoryView = currentInputAccessoryView + activeTextInput.reloadInputViews() + } } } diff --git a/ios/interactive/KeyboardOffsetProvider.swift b/ios/interactive/KeyboardOffsetProvider.swift index 88a91556a7..d134347dfb 100644 --- a/ios/interactive/KeyboardOffsetProvider.swift +++ b/ios/interactive/KeyboardOffsetProvider.swift @@ -16,6 +16,7 @@ public class KeyboardOffsetProvider: NSObject { private var offsetMap: [String: NSNumber] = [:] @objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber) { + KeyboardAreaExtender.shared.updateHeight(CGFloat(offset), nativeID) offsetMap[nativeID] = offset } diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 87773f3060..90c02ef165 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -36,15 +36,20 @@ public class KeyboardMovementObserver: NSObject { private var hasKVObserver = false private var isMounted = false // state variables - private var keyboardHeight: CGFloat = 0.0 + private var _keyboardHeight: CGFloat = 0.0 + private var keyboardHeight: CGFloat { + get { + print("\(_keyboardHeight) - \(KeyboardAreaExtender.shared.offset)") + return _keyboardHeight - KeyboardAreaExtender.shared.offset + } + set { + _keyboardHeight = newValue + } + } private var duration = 0 private var tag: NSNumber = -1 private var animation: KeyboardAnimation? private var didShowDeadline: Int64 = 0 - // TODO: should we move all interactive stuff into separate file? - // Or just move new functionality there? - // interactive keyboard - private var inputAccessoryView: UIView? @objc public init( handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void, @@ -125,7 +130,7 @@ public class KeyboardMovementObserver: NSObject { } // if keyboard height is not equal to its bounds - we can ignore // values, since they'll be invalid and will cause UI jumps - if keyboardView?.bounds.size.height != keyboardHeight { + if keyboardView?.bounds.size.height != _keyboardHeight { return } @@ -135,11 +140,12 @@ public class KeyboardMovementObserver: NSObject { let keyboardFrameY = changeValue.cgPointValue.y let keyboardWindowH = keyboardView?.window?.bounds.size.height ?? 0 let keyboardPosition = keyboardWindowH - keyboardFrameY + let position = CGFloat.interpolate( - inputRange: [keyboardHeight / 2, -keyboardHeight / 2], - outputRange: [keyboardHeight, 0], + inputRange: [_keyboardHeight / 2, -_keyboardHeight / 2], + outputRange: [_keyboardHeight, 0], currentValue: keyboardPosition - ) + ) - KeyboardAreaExtender.shared.offset if position == 0 { // it will be triggered before `keyboardWillDisappear` and @@ -149,7 +155,10 @@ public class KeyboardMovementObserver: NSObject { } prevKeyboardPosition = position - + /// TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? + (UIResponder.current?.inputAccessoryView as? InvisibleInputAccessoryView)?.updateHeight(to: 0) + UIResponder.current?.inputAccessoryView?.superview?.layoutIfNeeded() + /// onEvent( "onKeyboardMoveInteractive", position as NSNumber, @@ -176,11 +185,11 @@ public class KeyboardMovementObserver: NSObject { didShowDeadline = Date.currentTimeStamp + Int64(duration) onRequestAnimation() - onEvent("onKeyboardMoveStart", Float(keyboardHeight) as NSNumber, 1, duration as NSNumber, tag) - onNotify("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight, duration)) + onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag) + onNotify("KeyboardController::keyboardWillShow", getEventParams(self.keyboardHeight, duration)) setupKeyboardWatcher() - initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight) + initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight) } } @@ -204,12 +213,11 @@ public class KeyboardMovementObserver: NSObject { if let keyboardFrame = frame { let (position, _) = keyboardView.frameTransitionInWindow let keyboardHeight = keyboardFrame.cgRectValue.size.height - let responder = UIResponder.current - tag = responder.reactViewTag + tag = UIResponder.current.reactViewTag self.keyboardHeight = keyboardHeight // if the event is caught in between it's highly likely that it could be a "resize" event // so we just read actual keyboard frame value in this case - let height = timestamp >= didShowDeadline ? keyboardHeight : position + let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset // always limit progress to the maximum possible value let progress = min(height / self.keyboardHeight, 1.0) @@ -220,17 +228,6 @@ public class KeyboardMovementObserver: NSObject { removeKeyboardWatcher() setupKVObserver() animation = nil - - if let activeTextInput = responder as? TextInput, - let offset = KeyboardOffsetProvider.shared.getOffset( - forTextInputNativeID: responder.nativeID), - responder?.inputAccessoryView == nil - { - inputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(offset)) - - activeTextInput.inputAccessoryView = inputAccessoryView - activeTextInput.reloadInputViews() - } } } From ff7506d0dcf2b1c85651e4e8c623377d972525dc Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 4 Nov 2024 23:12:59 +0100 Subject: [PATCH 03/22] fix: lint, remove iav --- TODO | 4 ++ ios/extensions/UIResponder.swift | 4 +- .../InvisibleInputAccessoryView.swift | 70 +++++++++---------- ios/interactive/KeyboardAreaExtender.swift | 32 ++++----- ios/interactive/KeyboardOffsetProvider.swift | 48 ++++++------- ios/observers/KeyboardMovementObserver.swift | 3 +- ios/swizzling/UIResponderSwizzle.swift | 24 +++++-- src/reanimated.native.ts | 2 + 8 files changed, 102 insertions(+), 85 deletions(-) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000000..892d81f1fb --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +- (x) close after interactive gesture - sudden jump of 75 pixels -> fixed by detaching iav during `onInteractive` +- (x) keyboard hidden -> remove inputAccessoryView - done in resignFirstResponder +- show after interactive - keyboard height is 386 +- iOS 16 - after attaching fake iav we dispatch onEnd with height - 248 (298 - 50) diff --git a/ios/extensions/UIResponder.swift b/ios/extensions/UIResponder.swift index 6d3fa854c6..bf6263657d 100644 --- a/ios/extensions/UIResponder.swift +++ b/ios/extensions/UIResponder.swift @@ -44,11 +44,11 @@ public extension Optional where Wrapped == UIResponder { return (self as? UIView)?.superview?.reactTag ?? -1 #endif } - + var nativeID: String? { if let superview = (self as? UIView)?.superview as? UIView { return superview.nativeID as String? - } + } return nil } } diff --git a/ios/interactive/InvisibleInputAccessoryView.swift b/ios/interactive/InvisibleInputAccessoryView.swift index 58cd37defc..332d80b51f 100644 --- a/ios/interactive/InvisibleInputAccessoryView.swift +++ b/ios/interactive/InvisibleInputAccessoryView.swift @@ -9,42 +9,42 @@ import Foundation import UIKit public class InvisibleInputAccessoryView: UIView { - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + public convenience init(height: CGFloat) { - self.init(frame: CGRect.init(x: 0, y: 0, width: 0, height: height)) + self.init(frame: CGRect(x: 0, y: 0, width: 0, height: height)) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + override public func point(inside _: CGPoint, with _: UIEvent?) -> Bool { + // Return false to allow touch events to pass through + return false } - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupView() - } - - public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - // Return false to allow touch events to pass through - return false - } - - public func updateHeight(to newHeight: CGFloat) { - self.frame = CGRect.init(x: 0, y: 0, width: 0, height: newHeight) - - // Invalidate intrinsic content size to trigger a layout update - invalidateIntrinsicContentSize() - self.layoutIfNeeded() - } - - public override var intrinsicContentSize: CGSize { - return CGSize(width: UIView.noIntrinsicMetric, height: self.frame.height) - } - - private func setupView() { - isUserInteractionEnabled = false - // TODO: Set the background color to transparent - backgroundColor = UIColor.red - // self.backgroundColor = .clear - autoresizingMask = .flexibleHeight - } + public func updateHeight(to newHeight: CGFloat) { + frame = CGRect(x: 0, y: 0, width: 0, height: newHeight) + + // Invalidate intrinsic content size to trigger a layout update + invalidateIntrinsicContentSize() + layoutIfNeeded() + } + + override public var intrinsicContentSize: CGSize { + return CGSize(width: UIView.noIntrinsicMetric, height: frame.height) + } + + private func setupView() { + isUserInteractionEnabled = false + // TODO: Set the background color to transparent + backgroundColor = UIColor.red + // self.backgroundColor = .clear + autoresizingMask = .flexibleHeight + } } diff --git a/ios/interactive/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift index 5c114ab2d0..b2276fb82a 100644 --- a/ios/interactive/KeyboardAreaExtender.swift +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -5,12 +5,12 @@ // Created by Kiryl Ziusko on 02/11/2024. // -class KeyboardAreaExtender : NSObject { +class KeyboardAreaExtender: NSObject { private var currentInputAccessoryView: InvisibleInputAccessoryView? - + @objc public static let shared = KeyboardAreaExtender() - - private override init() { + + override private init() { super.init() NotificationCenter.default.addObserver( self, @@ -19,34 +19,32 @@ class KeyboardAreaExtender : NSObject { object: nil ) } - + deinit { NotificationCenter.default.removeObserver(self) } - + public var offset: CGFloat { - get { - return currentInputAccessoryView?.frame.height ?? 0 - } + return currentInputAccessoryView?.frame.height ?? 0 } - + public func hide() { // currentInputAccessoryView?.updateHeight(to: 0.0) // superview?.layoutIfNeeded() } - + public func updateHeight(_ to: CGFloat, _ nativeID: String) { - if (UIResponder.current.nativeID == nativeID) { + if UIResponder.current.nativeID == nativeID { currentInputAccessoryView?.updateHeight(to: to) } } - - @objc private func keyboardDidAppear(_ notification: Notification) { + + @objc private func keyboardDidAppear(_: Notification) { let responder = UIResponder.current if let activeTextInput = responder as? TextInput, - let offset = KeyboardOffsetProvider.shared.getOffset( - forTextInputNativeID: responder.nativeID), - responder?.inputAccessoryView == nil + let offset = KeyboardOffsetProvider.shared.getOffset( + forTextInputNativeID: responder.nativeID), + responder?.inputAccessoryView == nil { currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(offset)) diff --git a/ios/interactive/KeyboardOffsetProvider.swift b/ios/interactive/KeyboardOffsetProvider.swift index d134347dfb..92e784dbc1 100644 --- a/ios/interactive/KeyboardOffsetProvider.swift +++ b/ios/interactive/KeyboardOffsetProvider.swift @@ -9,28 +9,28 @@ import Foundation @objc(KeyboardOffsetProvider) public class KeyboardOffsetProvider: NSObject { - @objc public static let shared = KeyboardOffsetProvider() - - private override init() {} - - private var offsetMap: [String: NSNumber] = [:] - - @objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber) { - KeyboardAreaExtender.shared.updateHeight(CGFloat(offset), nativeID) - offsetMap[nativeID] = offset - } - - @objc public func getOffset(forTextInputNativeID nativeID: String?) -> NSNumber? { - guard let unwrappedNativeID = nativeID else { return nil } - return offsetMap[unwrappedNativeID] - } - - @objc public func hasOffset(forTextInputNativeID nativeID: String?) -> Bool { - guard let unwrappedNativeID = nativeID else { return false } - return offsetMap[unwrappedNativeID] != nil - } - - @objc public func removeOffset(forTextInputNativeID nativeID: String) { - offsetMap.removeValue(forKey: nativeID) - } + @objc public static let shared = KeyboardOffsetProvider() + + override private init() {} + + private var offsetMap: [String: NSNumber] = [:] + + @objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber) { + KeyboardAreaExtender.shared.updateHeight(CGFloat(offset), nativeID) + offsetMap[nativeID] = offset + } + + @objc public func getOffset(forTextInputNativeID nativeID: String?) -> NSNumber? { + guard let unwrappedNativeID = nativeID else { return nil } + return offsetMap[unwrappedNativeID] + } + + @objc public func hasOffset(forTextInputNativeID nativeID: String?) -> Bool { + guard let unwrappedNativeID = nativeID else { return false } + return offsetMap[unwrappedNativeID] != nil + } + + @objc public func removeOffset(forTextInputNativeID nativeID: String) { + offsetMap.removeValue(forKey: nativeID) + } } diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 90c02ef165..1ad56debf0 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -46,6 +46,7 @@ public class KeyboardMovementObserver: NSObject { _keyboardHeight = newValue } } + private var duration = 0 private var tag: NSNumber = -1 private var animation: KeyboardAnimation? @@ -155,7 +156,7 @@ public class KeyboardMovementObserver: NSObject { } prevKeyboardPosition = position - /// TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? + // TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? (UIResponder.current?.inputAccessoryView as? InvisibleInputAccessoryView)?.updateHeight(to: 0) UIResponder.current?.inputAccessoryView?.superview?.layoutIfNeeded() /// diff --git a/ios/swizzling/UIResponderSwizzle.swift b/ios/swizzling/UIResponderSwizzle.swift index 41e4ee6728..e1f8d3bbd3 100644 --- a/ios/swizzling/UIResponderSwizzle.swift +++ b/ios/swizzling/UIResponderSwizzle.swift @@ -1,5 +1,5 @@ // -// UIResponder.swift +// UIResponderSwizzle.swift // Pods // // Created by Kiryl Ziusko on 01/11/2024. @@ -22,6 +22,11 @@ extension UIResponder { originalResignFirstResponder = method_getImplementation(originalMethod) let swizzledImplementation: @convention(block) (UIResponder) -> Bool = { (self) in + // Check the type of inputAccessoryView and call original method immediately if not InvisibleInputAccessoryView + if let textField = self as? TextInput, !(textField.inputAccessoryView is InvisibleInputAccessoryView) { + return self.callOriginalResignFirstResponder(originalSelector) + } + // Add your custom behavior here print("Performing custom actions before the original resignFirstResponder") @@ -32,17 +37,24 @@ extension UIResponder { // Postpone execution of the original resignFirstResponder DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) { + (self as? TextInput)?.inputAccessoryView = nil // Call the original resignFirstResponder - typealias Function = @convention(c) (AnyObject, Selector) -> Bool - let castOriginalResignFirstResponder = unsafeBitCast( - originalResignFirstResponder, to: Function.self) - _ = castOriginalResignFirstResponder(self, originalSelector) + _ = self.callOriginalResignFirstResponder(originalSelector) } - return true // We need to return a value immediately, even though the actual action is delayed + // We need to return a value immediately, even though the actual action is delayed + return true } let implementation = imp_implementationWithBlock(swizzledImplementation) method_setImplementation(originalMethod, implementation) } + + private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool { + guard let originalResignFirstResponder = originalResignFirstResponder else { return false } + typealias Function = @convention(c) (AnyObject, Selector) -> Bool + let castOriginalResignFirstResponder = unsafeBitCast(originalResignFirstResponder, to: Function.self) + let result = castOriginalResignFirstResponder(self, selector) + return result + } } diff --git a/src/reanimated.native.ts b/src/reanimated.native.ts index b5600e3d41..8a6bc00a91 100644 --- a/src/reanimated.native.ts +++ b/src/reanimated.native.ts @@ -26,6 +26,8 @@ export const useAnimatedKeyboardHandler: KeyboardHandlerHook< onKeyboardMoveInteractive, } = handlers; + console.log(121212, event); + if ( onKeyboardMoveStart && event.eventName.endsWith("onKeyboardMoveStart") From c21568954b641d98a60113deaccd33116a1d4f60 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 4 Nov 2024 23:35:08 +0100 Subject: [PATCH 04/22] fix: iOS 16 incorrect height in onEnd event --- TODO | 3 ++- ios/extensions/Notification.swift | 4 +++ .../InvisibleInputAccessoryView.swift | 4 +-- ios/interactive/KeyboardAreaExtender.swift | 4 +++ ios/observers/KeyboardMovementObserver.swift | 26 +++++++++++++++++-- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/TODO b/TODO index 892d81f1fb..3d13a2b8d4 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,5 @@ - (x) close after interactive gesture - sudden jump of 75 pixels -> fixed by detaching iav during `onInteractive` - (x) keyboard hidden -> remove inputAccessoryView - done in resignFirstResponder +- (x) iOS 16 - after attaching fake iav we dispatch onEnd with height - 248 (298 - 50) -> ignore events after attaching iav, because in keyboardDidAppear `position` can be a random value (291 or 341) - show after interactive - keyboard height is 386 -- iOS 16 - after attaching fake iav we dispatch onEnd with height - 248 (298 - 50) +- text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) diff --git a/ios/extensions/Notification.swift b/ios/extensions/Notification.swift index fdf2a78b70..9874fd2296 100644 --- a/ios/extensions/Notification.swift +++ b/ios/extensions/Notification.swift @@ -15,3 +15,7 @@ extension Notification { return (duration, keyboardFrame) } } + +extension Notification.Name { + static let shouldIgnoreKeyboardEvents = Notification.Name("shouldIgnoreKeyboardEvents") +} diff --git a/ios/interactive/InvisibleInputAccessoryView.swift b/ios/interactive/InvisibleInputAccessoryView.swift index 332d80b51f..7ccd52b378 100644 --- a/ios/interactive/InvisibleInputAccessoryView.swift +++ b/ios/interactive/InvisibleInputAccessoryView.swift @@ -43,8 +43,8 @@ public class InvisibleInputAccessoryView: UIView { private func setupView() { isUserInteractionEnabled = false // TODO: Set the background color to transparent - backgroundColor = UIColor.red - // self.backgroundColor = .clear + // backgroundColor = UIColor.red + backgroundColor = .clear autoresizingMask = .flexibleHeight } } diff --git a/ios/interactive/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift index b2276fb82a..8a7b287b69 100644 --- a/ios/interactive/KeyboardAreaExtender.swift +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -50,6 +50,10 @@ class KeyboardAreaExtender: NSObject { activeTextInput.inputAccessoryView = currentInputAccessoryView activeTextInput.reloadInputViews() + + NotificationCenter.default.post(name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]) + + print("Attaching `inputAccessoryView` \(Date.currentTimeStamp)") } } } diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 1ad56debf0..f01dde99fc 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -51,6 +51,7 @@ public class KeyboardMovementObserver: NSObject { private var tag: NSNumber = -1 private var animation: KeyboardAnimation? private var didShowDeadline: Int64 = 0 + private var shouldIgnoreKeyboardEvents = false @objc public init( handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void, @@ -95,6 +96,11 @@ public class KeyboardMovementObserver: NSObject { name: UIResponder.keyboardDidHideNotification, object: nil ) + NotificationCenter.default.addObserver(forName: .shouldIgnoreKeyboardEvents, object: nil, queue: .main) { notification in + if let userInfo = notification.userInfo, let value = userInfo["ignore"] as? Bool { + self.shouldIgnoreKeyboardEvents = value + } + } } private func setupKVObserver() { @@ -129,9 +135,10 @@ public class KeyboardMovementObserver: NSObject { if displayLink != nil { return } + print("\(keyboardView?.bounds.size.height) \(_keyboardHeight)") // if keyboard height is not equal to its bounds - we can ignore // values, since they'll be invalid and will cause UI jumps - if keyboardView?.bounds.size.height != _keyboardHeight { + if (keyboardView?.bounds.size.height ?? 0) != _keyboardHeight { return } @@ -177,6 +184,8 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardWillAppear(_ notification: Notification) { + guard !shouldIgnoreKeyboardEvents else { return } + print("keyboardWillAppear \(Date.currentTimeStamp)") let (duration, frame) = notification.keyboardMetaData() if let keyboardFrame = frame { tag = UIResponder.current.reactViewTag @@ -195,6 +204,8 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardWillDisappear(_ notification: Notification) { + guard !shouldIgnoreKeyboardEvents else { return } + print("keyboardWillDisappear \(Date.currentTimeStamp)") let (duration, _) = notification.keyboardMetaData() tag = UIResponder.current.reactViewTag self.duration = duration @@ -209,6 +220,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardDidAppear(_ notification: Notification) { + print("keyboardDidAppear \(Date.currentTimeStamp)") let timestamp = Date.currentTimeStamp let (duration, frame) = notification.keyboardMetaData() if let keyboardFrame = frame { @@ -216,9 +228,17 @@ public class KeyboardMovementObserver: NSObject { let keyboardHeight = keyboardFrame.cgRectValue.size.height tag = UIResponder.current.reactViewTag self.keyboardHeight = keyboardHeight + + guard !shouldIgnoreKeyboardEvents else { + shouldIgnoreKeyboardEvents = false + return + } + // if the event is caught in between it's highly likely that it could be a "resize" event // so we just read actual keyboard frame value in this case - let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset + let height = timestamp >= didShowDeadline ? self.keyboardHeight : position + print("Using \(timestamp >= didShowDeadline ? "self.keyboardHeight" : "position")") + print("\(timestamp) vs \(didShowDeadline)") // always limit progress to the maximum possible value let progress = min(height / self.keyboardHeight, 1.0) @@ -233,6 +253,8 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardDidDisappear(_ notification: Notification) { + guard !shouldIgnoreKeyboardEvents else { return } + print("keyboardDidDisappear \(Date.currentTimeStamp)") let (duration, _) = notification.keyboardMetaData() tag = UIResponder.current.reactViewTag From b90aaa9f0bf5391e7fd7a211de5ef7ff7ddbe956 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 7 Nov 2024 11:11:00 +0100 Subject: [PATCH 05/22] chore: update TODO --- TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO b/TODO index 3d13a2b8d4..e845126fea 100644 --- a/TODO +++ b/TODO @@ -2,4 +2,4 @@ - (x) keyboard hidden -> remove inputAccessoryView - done in resignFirstResponder - (x) iOS 16 - after attaching fake iav we dispatch onEnd with height - 248 (298 - 50) -> ignore events after attaching iav, because in keyboardDidAppear `position` can be a random value (291 or 341) - show after interactive - keyboard height is 386 -- text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) +- text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) - maybe because of frequent conversions to CGFloat? From 56116e491f4586e7f0ebec17342e7fcc04cfabc9 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 8 Nov 2024 23:42:38 +0100 Subject: [PATCH 06/22] fix: grow of input breaks `onInteractive` handler --- TODO | 2 +- ios/interactive/InvisibleInputAccessoryView.swift | 5 +++++ ios/observers/KeyboardMovementObserver.swift | 5 ++--- ios/swizzling/UIResponderSwizzle.swift | 3 +-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/TODO b/TODO index e845126fea..9f3e2f99aa 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,5 @@ - (x) close after interactive gesture - sudden jump of 75 pixels -> fixed by detaching iav during `onInteractive` - (x) keyboard hidden -> remove inputAccessoryView - done in resignFirstResponder - (x) iOS 16 - after attaching fake iav we dispatch onEnd with height - 248 (298 - 50) -> ignore events after attaching iav, because in keyboardDidAppear `position` can be a random value (291 or 341) +- (x) text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) - maybe because of frequent conversions to CGFloat? <- fixed via rounding - show after interactive - keyboard height is 386 -- text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) - maybe because of frequent conversions to CGFloat? diff --git a/ios/interactive/InvisibleInputAccessoryView.swift b/ios/interactive/InvisibleInputAccessoryView.swift index 7ccd52b378..5ed8387306 100644 --- a/ios/interactive/InvisibleInputAccessoryView.swift +++ b/ios/interactive/InvisibleInputAccessoryView.swift @@ -36,6 +36,11 @@ public class InvisibleInputAccessoryView: UIView { layoutIfNeeded() } + public func hide() { + updateHeight(to: 0.0) + superview?.layoutIfNeeded() + } + override public var intrinsicContentSize: CGSize { return CGSize(width: UIView.noIntrinsicMetric, height: frame.height) } diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index f01dde99fc..d4b2312398 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -138,7 +138,7 @@ public class KeyboardMovementObserver: NSObject { print("\(keyboardView?.bounds.size.height) \(_keyboardHeight)") // if keyboard height is not equal to its bounds - we can ignore // values, since they'll be invalid and will cause UI jumps - if (keyboardView?.bounds.size.height ?? 0) != _keyboardHeight { + if floor(keyboardView?.bounds.size.height ?? 0) != floor(_keyboardHeight) { return } @@ -164,8 +164,7 @@ public class KeyboardMovementObserver: NSObject { prevKeyboardPosition = position // TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? - (UIResponder.current?.inputAccessoryView as? InvisibleInputAccessoryView)?.updateHeight(to: 0) - UIResponder.current?.inputAccessoryView?.superview?.layoutIfNeeded() + (UIResponder.current?.inputAccessoryView as? InvisibleInputAccessoryView)?.hide() /// onEvent( "onKeyboardMoveInteractive", diff --git a/ios/swizzling/UIResponderSwizzle.swift b/ios/swizzling/UIResponderSwizzle.swift index e1f8d3bbd3..735b3b136c 100644 --- a/ios/swizzling/UIResponderSwizzle.swift +++ b/ios/swizzling/UIResponderSwizzle.swift @@ -31,8 +31,7 @@ extension UIResponder { print("Performing custom actions before the original resignFirstResponder") if let textField = self as? TextInput { - (textField.inputAccessoryView as? InvisibleInputAccessoryView)?.updateHeight(to: 0) - textField.inputAccessoryView?.superview?.layoutIfNeeded() + (textField.inputAccessoryView as? InvisibleInputAccessoryView)?.hide() } // Postpone execution of the original resignFirstResponder From 32d8e4e8d8a4dbfa2aead76345be8142c65edddd Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 11:38:40 +0100 Subject: [PATCH 07/22] docs: document new API --- docs/docs/api/keyboard-gesture-area.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/docs/api/keyboard-gesture-area.md b/docs/docs/api/keyboard-gesture-area.md index 6c14e7f174..68c51a7089 100644 --- a/docs/docs/api/keyboard-gesture-area.md +++ b/docs/docs/api/keyboard-gesture-area.md @@ -10,12 +10,6 @@ keywords: ] --- - - - -# KeyboardGestureArea
- - `KeyboardGestureArea` allows you to define a region on the screen, where gestures will control the keyboard position. :::info Platform availability @@ -28,27 +22,32 @@ This component is available only for Android >= 11. For iOS and Android < 11 it Extra distance to the keyboard. Default is `0`. -### `interpolator` +### `interpolator`
String with possible values `linear` and `ios`: - **ios** - interactive keyboard dismissing will work as in iOS: swipes in non-keyboard area will not affect keyboard positioning, but if your swipe touches keyboard - keyboard will follow finger position. - **linear** - gestures inside the component will linearly affect the position of the keyboard, i.e. if the user swipes down by 20 pixels, then the keyboard will also be moved down by 20 pixels, even if the gesture was not made over the keyboard area. -### `showOnSwipeUp` +### `showOnSwipeUp`
A boolean prop which allows to customize interactive keyboard behavior. If set to `true` then it allows to show keyboard (if it's already closed) by swipe up gesture. `false` by default. -### `enableSwipeToDismiss` +### `enableSwipeToDismiss`
A boolean prop which allows to customize interactive keyboard behavior. If set to `false`, then any gesture will not affect keyboard position if the keyboard is shown. `true` by default. +### `textInputNativeID`
+ +A corresponding `nativeID` value from the corresponding `TextInput`. + ## Example ```tsx - + {/* The other UI components of application in your tree */} + ``` From 82b0501b44695f2b83b19d4cc631e32d0c32bc96 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 11:39:39 +0100 Subject: [PATCH 08/22] fix: objc lint --- ios/views/KeyboardGestureAreaManager.h | 4 ++-- ios/views/KeyboardGestureAreaManager.mm | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ios/views/KeyboardGestureAreaManager.h b/ios/views/KeyboardGestureAreaManager.h index 63b98199e8..1de6169a4e 100644 --- a/ios/views/KeyboardGestureAreaManager.h +++ b/ios/views/KeyboardGestureAreaManager.h @@ -26,6 +26,6 @@ #endif // TODO: should be array? How to specify multiple different offsets for different inputs? -@property (nonatomic, assign) NSNumber* offset; -@property (nonatomic, assign) NSString* textInputNativeID; +@property (nonatomic, assign) NSNumber *offset; +@property (nonatomic, assign) NSString *textInputNativeID; @end diff --git a/ios/views/KeyboardGestureAreaManager.mm b/ios/views/KeyboardGestureAreaManager.mm index 06754e7f94..b1d877ffa8 100644 --- a/ios/views/KeyboardGestureAreaManager.mm +++ b/ios/views/KeyboardGestureAreaManager.mm @@ -34,8 +34,8 @@ @implementation KeyboardGestureAreaManager RCT_EXPORT_MODULE(KeyboardGestureAreaManager) // Expose props to React Native -RCT_EXPORT_VIEW_PROPERTY(textInputNativeID, NSString*) -RCT_EXPORT_VIEW_PROPERTY(offset, NSNumber*) +RCT_EXPORT_VIEW_PROPERTY(textInputNativeID, NSString *) +RCT_EXPORT_VIEW_PROPERTY(offset, NSNumber *) + (BOOL)requiresMainQueueSetup { @@ -81,7 +81,6 @@ + (void)load - (instancetype)init { if (self = [super init]) { - } return self; } @@ -90,7 +89,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { self = [super initWithFrame:CGRectZero]; if (self) { - } return self; @@ -122,14 +120,14 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & [super updateProps:props oldProps:oldProps]; } #else -- (void)setOffset:(NSNumber*)offset +- (void)setOffset:(NSNumber *)offset { // [[KeyboardOffsetProvider shared] removeOffsetForTextInputNativeID:_textInputNativeID]; [[KeyboardOffsetProvider shared] setOffsetForTextInputNativeID:_textInputNativeID offset:offset]; _offset = offset; } -- (void)setTextInputNativeID:(NSString*)textInputNativeID +- (void)setTextInputNativeID:(NSString *)textInputNativeID { [[KeyboardOffsetProvider shared] removeOffsetForTextInputNativeID:_textInputNativeID]; [[KeyboardOffsetProvider shared] setOffsetForTextInputNativeID:textInputNativeID offset:_offset]; From 7556b7b381f9843eca3fda83a6fa55a9b6a017e5 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 11:41:49 +0100 Subject: [PATCH 09/22] fix: lint ts --- example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx b/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx index 841d39a96b..282cfa2019 100644 --- a/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx +++ b/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useRef, useState } from "react"; -import { TextInput, View } from "react-native"; +import { TextInput } from "react-native"; import { KeyboardGestureArea, useKeyboardHandler, From 0b59cde666e31753497c7c5d2c09cfee50a15ce1 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 11:44:59 +0100 Subject: [PATCH 10/22] fix: platform inconsistency, fabric Android build --- .../KeyboardGestureAreaViewManager.kt | 8 ++++++++ .../KeyboardGestureAreaViewManager.kt | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt index aeaa64d732..d53028b06e 100644 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt @@ -56,4 +56,12 @@ class KeyboardGestureAreaViewManager( ) { manager.setScrollKeyboardOffScreenWhenVisible(view as KeyboardGestureAreaReactViewGroup, value) } + + @ReactProp(name = "textInputNativeID") + override fun setTextInputNativeID( + view: ReactViewGroup?, + value: String, + ) { + // no-op + } } diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt index 996485ac5f..135c482c3e 100644 --- a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt @@ -48,4 +48,12 @@ class KeyboardGestureAreaViewManager( ) { manager.setScrollKeyboardOffScreenWhenVisible(view, value) } + + @ReactProp(name = "textInputNativeID") + fun setTextInputNativeID( + view: KeyboardGestureAreaReactViewGroup, + value: String, + ) { + // no-op + } } From ef6777a3e89d67c3d7eac971df36e487381ec658 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 13:48:02 +0100 Subject: [PATCH 11/22] fix: lint ts --- docs/docs/api/keyboard-gesture-area.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/api/keyboard-gesture-area.md b/docs/docs/api/keyboard-gesture-area.md index 68c51a7089..10ac17b169 100644 --- a/docs/docs/api/keyboard-gesture-area.md +++ b/docs/docs/api/keyboard-gesture-area.md @@ -44,7 +44,11 @@ A corresponding `nativeID` value from the corresponding `TextInput`. ## Example ```tsx - + {/* The other UI components of application in your tree */} From bd1ff10756dcbeebfbf977b3fe516f1c9244bd19 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 13:48:09 +0100 Subject: [PATCH 12/22] fix: fabric build --- .../KeyboardGestureAreaViewManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt index d53028b06e..69adb321de 100644 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt @@ -59,8 +59,8 @@ class KeyboardGestureAreaViewManager( @ReactProp(name = "textInputNativeID") override fun setTextInputNativeID( - view: ReactViewGroup?, - value: String, + view: ReactViewGroup, + value: String?, ) { // no-op } From 8cf282f727df243513d5b275e4f3f9193e450812 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 13:51:25 +0100 Subject: [PATCH 13/22] fix: suppress detekt violation --- .../KeyboardGestureAreaViewManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt index 135c482c3e..1c0c79f956 100644 --- a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt @@ -49,6 +49,7 @@ class KeyboardGestureAreaViewManager( manager.setScrollKeyboardOffScreenWhenVisible(view, value) } + @Suppress("detekt:UnusedParameter") @ReactProp(name = "textInputNativeID") fun setTextInputNativeID( view: KeyboardGestureAreaReactViewGroup, From 3e0876f69e89bdd8234264afec4dafbfe01242bb Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 15:15:14 +0100 Subject: [PATCH 14/22] fix: conflicts after rebasing to main --- ios/protocols/TextInput.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/protocols/TextInput.swift b/ios/protocols/TextInput.swift index be7707667b..b142e5754b 100644 --- a/ios/protocols/TextInput.swift +++ b/ios/protocols/TextInput.swift @@ -1,8 +1,9 @@ // // TextInput.swift -// Pods +// KeyboardController // -// Created by Kiryl Ziusko on 01/11/2024. +// Created by Kiryl Ziusko on 27/01/2024. +// Copyright © 2024 Facebook. All rights reserved. // public protocol TextInput: AnyObject { From d701ee3a578f212c33d4bcf64f95e43d518f9079 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 17:41:55 +0100 Subject: [PATCH 15/22] fix: iOS unit tests --- ios/protocols/TextInput.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ios/protocols/TextInput.swift b/ios/protocols/TextInput.swift index b142e5754b..023de40c21 100644 --- a/ios/protocols/TextInput.swift +++ b/ios/protocols/TextInput.swift @@ -6,6 +6,9 @@ // Copyright © 2024 Facebook. All rights reserved. // +import Foundation +import UIKit + public protocol TextInput: AnyObject { // default common methods/properties var inputAccessoryView: UIView? { get set } From 4f5d77bd5ef2bbf0f44b1e94a481f4a655edf1cb Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sat, 16 Nov 2024 18:00:47 +0100 Subject: [PATCH 16/22] fix: mock `nativeID` to pass iOS unit tests --- .../KeyboardControllerNative/Extension+UIView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift b/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift index ee010d42a2..fcb3f8b2af 100644 --- a/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift +++ b/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift @@ -12,4 +12,8 @@ public extension UIView { var reactTag: NSNumber { return tag as NSNumber } + + var nativeID: String { + return accessibilityIdentifier ?? "" + } } From 3846dc88403b3c5a5f72c0f9ecec9f0ca38c5992 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 18 Nov 2024 09:13:19 +0100 Subject: [PATCH 17/22] fix: swiftformat --- .../KeyboardControllerNative/Extension+UIView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift b/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift index fcb3f8b2af..056035136a 100644 --- a/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift +++ b/ios/KeyboardControllerNative/KeyboardControllerNative/Extension+UIView.swift @@ -12,7 +12,7 @@ public extension UIView { var reactTag: NSNumber { return tag as NSNumber } - + var nativeID: String { return accessibilityIdentifier ?? "" } From 1d87523d1437913eb5ae8f2f39b0376912ed82ba Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sun, 24 Nov 2024 15:39:54 +0100 Subject: [PATCH 18/22] refactor: create separate class to simplify KeyboardMovementObserver implementation --- ios/observers/KeyboardEventsIgnorer.swift | 31 ++++++++++++++++++++ ios/observers/KeyboardMovementObserver.swift | 18 +++++------- 2 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 ios/observers/KeyboardEventsIgnorer.swift diff --git a/ios/observers/KeyboardEventsIgnorer.swift b/ios/observers/KeyboardEventsIgnorer.swift new file mode 100644 index 0000000000..6445c48c8e --- /dev/null +++ b/ios/observers/KeyboardEventsIgnorer.swift @@ -0,0 +1,31 @@ +// +// KeyboardEventsIgnorer.swift +// Pods +// +// Created by Kiryl Ziusko on 24/11/2024. +// + +import Foundation + +class KeyboardEventsIgnorer { + var shouldIgnoreKeyboardEvents = false + + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleIgnoreKeyboardEventsNotification), + name: .shouldIgnoreKeyboardEvents, + object: nil + ) + } + + @objc private func handleIgnoreKeyboardEventsNotification(_ notification: Notification) { + if let userInfo = notification.userInfo, let value = userInfo["ignore"] as? Bool { + shouldIgnoreKeyboardEvents = value + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index d4b2312398..ee85c6c243 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -51,7 +51,8 @@ public class KeyboardMovementObserver: NSObject { private var tag: NSNumber = -1 private var animation: KeyboardAnimation? private var didShowDeadline: Int64 = 0 - private var shouldIgnoreKeyboardEvents = false + // class intances + private let eventsIgnorer = KeyboardEventsIgnorer() @objc public init( handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void, @@ -96,11 +97,6 @@ public class KeyboardMovementObserver: NSObject { name: UIResponder.keyboardDidHideNotification, object: nil ) - NotificationCenter.default.addObserver(forName: .shouldIgnoreKeyboardEvents, object: nil, queue: .main) { notification in - if let userInfo = notification.userInfo, let value = userInfo["ignore"] as? Bool { - self.shouldIgnoreKeyboardEvents = value - } - } } private func setupKVObserver() { @@ -183,7 +179,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardWillAppear(_ notification: Notification) { - guard !shouldIgnoreKeyboardEvents else { return } + guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { return } print("keyboardWillAppear \(Date.currentTimeStamp)") let (duration, frame) = notification.keyboardMetaData() if let keyboardFrame = frame { @@ -203,7 +199,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardWillDisappear(_ notification: Notification) { - guard !shouldIgnoreKeyboardEvents else { return } + guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { return } print("keyboardWillDisappear \(Date.currentTimeStamp)") let (duration, _) = notification.keyboardMetaData() tag = UIResponder.current.reactViewTag @@ -228,8 +224,8 @@ public class KeyboardMovementObserver: NSObject { tag = UIResponder.current.reactViewTag self.keyboardHeight = keyboardHeight - guard !shouldIgnoreKeyboardEvents else { - shouldIgnoreKeyboardEvents = false + guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { + eventsIgnorer.shouldIgnoreKeyboardEvents = false return } @@ -252,7 +248,7 @@ public class KeyboardMovementObserver: NSObject { } @objc func keyboardDidDisappear(_ notification: Notification) { - guard !shouldIgnoreKeyboardEvents else { return } + guard !eventsIgnorer.shouldIgnoreKeyboardEvents else { return } print("keyboardDidDisappear \(Date.currentTimeStamp)") let (duration, _) = notification.keyboardMetaData() tag = UIResponder.current.reactViewTag From 9c6dbd6f0bb6cb06322134b32b02be045104c258 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Sun, 24 Nov 2024 16:38:06 +0100 Subject: [PATCH 19/22] refactor: keep progress --- ios/interactive/KeyboardAreaExtender.swift | 5 ++--- ios/observers/KeyboardMovementObserver.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ios/interactive/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift index 8a7b287b69..3068fb19a8 100644 --- a/ios/interactive/KeyboardAreaExtender.swift +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -29,8 +29,7 @@ class KeyboardAreaExtender: NSObject { } public func hide() { - // currentInputAccessoryView?.updateHeight(to: 0.0) - // superview?.layoutIfNeeded() + currentInputAccessoryView?.hide() } public func updateHeight(_ to: CGFloat, _ nativeID: String) { @@ -46,7 +45,7 @@ class KeyboardAreaExtender: NSObject { forTextInputNativeID: responder.nativeID), responder?.inputAccessoryView == nil { - currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(offset)) + currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(truncating: offset)) activeTextInput.inputAccessoryView = currentInputAccessoryView activeTextInput.reloadInputViews() diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index ee85c6c243..71ed123beb 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -51,7 +51,7 @@ public class KeyboardMovementObserver: NSObject { private var tag: NSNumber = -1 private var animation: KeyboardAnimation? private var didShowDeadline: Int64 = 0 - // class intances + // external class instances private let eventsIgnorer = KeyboardEventsIgnorer() @objc public init( @@ -160,7 +160,7 @@ public class KeyboardMovementObserver: NSObject { prevKeyboardPosition = position // TODO: needs here? Why in onStart/onEnd after interactive gesture we get keyboard height as 386? - (UIResponder.current?.inputAccessoryView as? InvisibleInputAccessoryView)?.hide() + KeyboardAreaExtender.shared.hide() /// onEvent( "onKeyboardMoveInteractive", From 10c933aa4bde45553ab5af625ef89e7396ffc23e Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 25 Nov 2024 00:22:13 +0100 Subject: [PATCH 20/22] chore: update list of bugs --- TODO | 9 ++++++++- ios/interactive/KeyboardAreaExtender.swift | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 9f3e2f99aa..2207d1c228 100644 --- a/TODO +++ b/TODO @@ -2,4 +2,11 @@ - (x) keyboard hidden -> remove inputAccessoryView - done in resignFirstResponder - (x) iOS 16 - after attaching fake iav we dispatch onEnd with height - 248 (298 - 50) -> ignore events after attaching iav, because in keyboardDidAppear `position` can be a random value (291 or 341) - (x) text input grow -> can not make interactive gesture (Optional(424.6666666666667) 424.66666666666674) - maybe because of frequent conversions to CGFloat? <- fixed via rounding -- show after interactive - keyboard height is 386 +- 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) + +:: call actual code for hide only once +::: 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/KeyboardAreaExtender.swift b/ios/interactive/KeyboardAreaExtender.swift index 3068fb19a8..5433e90fd2 100644 --- a/ios/interactive/KeyboardAreaExtender.swift +++ b/ios/interactive/KeyboardAreaExtender.swift @@ -39,6 +39,7 @@ class KeyboardAreaExtender: NSObject { } @objc private func keyboardDidAppear(_: Notification) { + print("KEA - keyboardDidAppear \(Date.currentTimeStamp)") let responder = UIResponder.current if let activeTextInput = responder as? TextInput, let offset = KeyboardOffsetProvider.shared.getOffset( @@ -50,7 +51,9 @@ class KeyboardAreaExtender: NSObject { activeTextInput.inputAccessoryView = currentInputAccessoryView activeTextInput.reloadInputViews() - NotificationCenter.default.post(name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]) + NotificationCenter.default.post( + name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true] + ) print("Attaching `inputAccessoryView` \(Date.currentTimeStamp)") } From 6ba2ee014375f1082b0d14af761c5c73fbd21447 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 26 Nov 2024 10:37:16 +0100 Subject: [PATCH 21/22] 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 71ed123beb..15b96c2e09 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, @@ -159,9 +157,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, @@ -169,6 +165,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() + /// } } @@ -179,7 +179,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 { @@ -199,7 +199,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 @@ -224,8 +224,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 } @@ -248,7 +248,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 { From 9552ce0e3f63bfff709ce4fb3e077b3057b34b0c Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 26 Nov 2024 15:27:09 +0100 Subject: [PATCH 22/22] feat: try to de-attach input accessory view only when keyboard got closed (onMove handler still not precise) --- .../Examples/InteractiveKeyboardIOS/index.tsx | 12 ++++++------ ios/interactive/InvisibleInputAccessoryView.swift | 4 ++-- ios/observers/KeyboardMovementObserver.swift | 14 ++++++-------- ios/swizzling/UIResponderSwizzle.swift | 3 --- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx b/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx index 282cfa2019..7784affc10 100644 --- a/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx +++ b/example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx @@ -43,8 +43,8 @@ const useKeyboardAnimation = () => { return; } - progress.value = e.progress; - height.value = e.height; + // progress.value = e.progress; + // height.value = e.height; inset.value = e.height; // Math.max is needed to prevent overscroll when keyboard hides (and user scrolled to the top, for example) @@ -59,10 +59,10 @@ const useKeyboardAnimation = () => { onMove: (e) => { "worklet"; - if (shouldUseOnMoveHandler.value) { - progress.value = e.progress; - height.value = e.height; - } + // if (shouldUseOnMoveHandler.value) { + progress.value = e.progress; + height.value = e.height; + // } }, onEnd: (e) => { "worklet"; diff --git a/ios/interactive/InvisibleInputAccessoryView.swift b/ios/interactive/InvisibleInputAccessoryView.swift index c9e6ab04e3..fe0ab57fc1 100644 --- a/ios/interactive/InvisibleInputAccessoryView.swift +++ b/ios/interactive/InvisibleInputAccessoryView.swift @@ -53,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.withAlphaComponent(0.2) + backgroundColor = .clear autoresizingMask = .flexibleHeight } } diff --git a/ios/observers/KeyboardMovementObserver.swift b/ios/observers/KeyboardMovementObserver.swift index 15b96c2e09..4856779e06 100644 --- a/ios/observers/KeyboardMovementObserver.swift +++ b/ios/observers/KeyboardMovementObserver.swift @@ -165,10 +165,6 @@ 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() - /// } } @@ -211,7 +207,7 @@ public class KeyboardMovementObserver: NSObject { setupKeyboardWatcher() removeKVObserver() - initializeAnimation(fromValue: prevKeyboardPosition, toValue: 0) + initializeAnimation(fromValue: prevKeyboardPosition + KeyboardAreaExtender.shared.offset, toValue: 0) } @objc func keyboardDidAppear(_ notification: Notification) { @@ -231,7 +227,7 @@ public class KeyboardMovementObserver: NSObject { // if the event is caught in between it's highly likely that it could be a "resize" event // so we just read actual keyboard frame value in this case - let height = timestamp >= didShowDeadline ? self.keyboardHeight : position + let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset print("Using \(timestamp >= didShowDeadline ? "self.keyboardHeight" : "position")") print("\(timestamp) vs \(didShowDeadline)") // always limit progress to the maximum possible value @@ -305,7 +301,7 @@ public class KeyboardMovementObserver: NSObject { } if animation == nil { - initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight) + initializeAnimation(fromValue: prevKeyboardPosition, toValue: self._keyboardHeight) } prevKeyboardPosition = keyboardPosition @@ -329,7 +325,9 @@ public class KeyboardMovementObserver: NSObject { // handles a case when final frame has final destination (i. e. 0 or 291) // but CASpringAnimation can never get to this final destination let race: (CGFloat, CGFloat) -> CGFloat = animation.isIncreasing ? max : min - keyboardPosition = race(position, keyboardPosition) + keyboardPosition = race(position, keyboardPosition) - KeyboardAreaExtender.shared.offset + + print("\(visibleKeyboardHeight) -> \(position)") } onEvent( diff --git a/ios/swizzling/UIResponderSwizzle.swift b/ios/swizzling/UIResponderSwizzle.swift index c2668171e6..3ffa7fdba6 100644 --- a/ios/swizzling/UIResponderSwizzle.swift +++ b/ios/swizzling/UIResponderSwizzle.swift @@ -31,9 +31,6 @@ extension UIResponder { print("Performing custom actions before the original resignFirstResponder") 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) {