react-native-keyboard-controller 1.15.2 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +6 -0
  2. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +8 -0
  3. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +77 -51
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +3 -3
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +2 -2
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +7 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +9 -1
  8. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +8 -0
  9. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +9 -0
  10. package/ios/animations/KeyboardAnimation.swift +4 -4
  11. package/ios/animations/SpringAnimation.swift +28 -5
  12. package/ios/animations/TimingAnimation.swift +77 -32
  13. package/ios/delegates/KCTextInputCompositeDelegate.swift +73 -1
  14. package/ios/extensions/Notification.swift +4 -0
  15. package/ios/extensions/UIResponder.swift +14 -0
  16. package/ios/interactive/InvisibleInputAccessoryView.swift +59 -0
  17. package/ios/interactive/KeyboardAreaExtender.swift +74 -0
  18. package/ios/interactive/KeyboardOffsetProvider.swift +39 -0
  19. package/ios/observers/FocusedInputObserver.swift +81 -25
  20. package/ios/observers/KeyboardEventsIgnorer.swift +39 -0
  21. package/ios/observers/KeyboardMovementObserver.swift +59 -61
  22. package/ios/protocols/TextInput.swift +2 -0
  23. package/ios/swizzling/UIResponderSwizzle.swift +60 -0
  24. package/ios/traversal/ViewHierarchyNavigator.swift +1 -1
  25. package/ios/views/KeyboardGestureAreaManager.h +31 -0
  26. package/ios/views/KeyboardGestureAreaManager.mm +174 -0
  27. package/lib/commonjs/animated.js +4 -1
  28. package/lib/commonjs/animated.js.map +1 -1
  29. package/lib/commonjs/bindings.native.js +1 -1
  30. package/lib/commonjs/bindings.native.js.map +1 -1
  31. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  32. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js +1 -3
  33. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  34. package/lib/commonjs/types.js.map +1 -1
  35. package/lib/module/animated.js +4 -1
  36. package/lib/module/animated.js.map +1 -1
  37. package/lib/module/bindings.native.js +1 -1
  38. package/lib/module/bindings.native.js.map +1 -1
  39. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  40. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js +1 -3
  41. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  42. package/lib/module/types.js.map +1 -1
  43. package/lib/typescript/animated.d.ts +9 -1
  44. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +1 -0
  45. package/lib/typescript/specs/KeyboardGestureAreaNativeComponent.d.ts +1 -0
  46. package/lib/typescript/types.d.ts +6 -1
  47. package/package.json +1 -1
  48. package/src/animated.tsx +15 -1
  49. package/src/bindings.native.ts +1 -1
  50. package/src/specs/KeyboardControllerViewNativeComponent.ts +1 -0
  51. package/src/specs/KeyboardGestureAreaNativeComponent.ts +4 -3
  52. package/src/types.ts +6 -1
  53. package/android/src/main/java/com/reactnativekeyboardcontroller/ui/FrameScheduler.kt +0 -43
@@ -44,6 +44,20 @@ public extension Optional where Wrapped == UIResponder {
44
44
  return (self as? UIView)?.superview?.reactTag ?? -1
45
45
  #endif
46
46
  }
47
+
48
+ var nativeID: String? {
49
+ guard let superview = (self as? UIView)?.superview else { return nil }
50
+
51
+ #if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
52
+ if superview.responds(to: Selector(("nativeId"))) == true {
53
+ return (superview as NSObject).value(forKey: "nativeId") as? String
54
+ }
55
+
56
+ return nil
57
+ #else
58
+ return superview.nativeID
59
+ #endif
60
+ }
47
61
  }
48
62
 
49
63
  public extension Optional where Wrapped: UIResponder {
@@ -0,0 +1,59 @@
1
+ //
2
+ // InvisibleInputAccessoryView.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ public class InvisibleInputAccessoryView: UIView {
12
+ var isShown = true
13
+
14
+ override init(frame: CGRect) {
15
+ super.init(frame: frame)
16
+ setupView()
17
+ }
18
+
19
+ public convenience init(height: CGFloat) {
20
+ self.init(frame: CGRect(x: 0, y: 0, width: 0, height: height))
21
+ }
22
+
23
+ required init?(coder aDecoder: NSCoder) {
24
+ super.init(coder: aDecoder)
25
+ setupView()
26
+ }
27
+
28
+ override public func point(inside _: CGPoint, with _: UIEvent?) -> Bool {
29
+ // Return false to allow touch events to pass through
30
+ return false
31
+ }
32
+
33
+ public func updateHeight(to newHeight: CGFloat) {
34
+ frame = CGRect(x: 0, y: 0, width: 0, height: newHeight)
35
+
36
+ // Invalidate intrinsic content size to trigger a layout update
37
+ invalidateIntrinsicContentSize()
38
+ layoutIfNeeded()
39
+ }
40
+
41
+ public func hide() {
42
+ guard isShown else { return }
43
+ isShown = false
44
+ updateHeight(to: 0.0)
45
+ superview?.layoutIfNeeded()
46
+ }
47
+
48
+ override public var intrinsicContentSize: CGSize {
49
+ return CGSize(width: UIView.noIntrinsicMetric, height: frame.height)
50
+ }
51
+
52
+ private func setupView() {
53
+ isUserInteractionEnabled = false
54
+ // for debug purposes
55
+ // backgroundColor = UIColor.red.withAlphaComponent(0.2)
56
+ backgroundColor = .clear
57
+ autoresizingMask = .flexibleHeight
58
+ }
59
+ }
@@ -0,0 +1,74 @@
1
+ //
2
+ // KeyboardAreaExtender.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 02/11/2024.
6
+ //
7
+
8
+ class KeyboardAreaExtender: NSObject {
9
+ private var currentInputAccessoryView: InvisibleInputAccessoryView?
10
+
11
+ @objc public static let shared = KeyboardAreaExtender()
12
+
13
+ override private init() {
14
+ super.init()
15
+ NotificationCenter.default.addObserver(
16
+ self,
17
+ selector: #selector(keyboardDidAppear),
18
+ name: UIResponder.keyboardDidShowNotification,
19
+ object: nil
20
+ )
21
+ }
22
+
23
+ deinit {
24
+ NotificationCenter.default.removeObserver(self)
25
+ }
26
+
27
+ public var offset: CGFloat {
28
+ return currentInputAccessoryView?.frame.height ?? 0
29
+ }
30
+
31
+ public func hide() {
32
+ if isVisible {
33
+ NotificationCenter.default.post(
34
+ name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]
35
+ )
36
+ currentInputAccessoryView?.hide()
37
+ }
38
+ }
39
+
40
+ public func remove() {
41
+ currentInputAccessoryView = nil
42
+ }
43
+
44
+ public func updateHeight(to newHeight: CGFloat, for nativeID: String) {
45
+ if UIResponder.current.nativeID == nativeID {
46
+ currentInputAccessoryView?.updateHeight(to: newHeight)
47
+ }
48
+ }
49
+
50
+ private var isVisible: Bool {
51
+ return currentInputAccessoryView?.isShown ?? false
52
+ }
53
+
54
+ @objc private func keyboardDidAppear(_: Notification) {
55
+ let responder = UIResponder.current
56
+ if let activeTextInput = responder as? TextInput,
57
+ let offset = KeyboardOffsetProvider.shared.getOffset(
58
+ forTextInputNativeID: responder.nativeID),
59
+ responder?.inputAccessoryView == nil
60
+ {
61
+ currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(truncating: offset))
62
+
63
+ // we need to send this event before we actually attach the IAV
64
+ // since on some OS versions (iOS 15 for example), `reloadInputViews`
65
+ // will trigger `keyboardDidAppear` listener immediately
66
+ NotificationCenter.default.post(
67
+ name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]
68
+ )
69
+
70
+ activeTextInput.inputAccessoryView = currentInputAccessoryView
71
+ activeTextInput.reloadInputViews()
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,39 @@
1
+ //
2
+ // KeyboardOffsetProvider.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ @objc(KeyboardOffsetProvider)
11
+ public class KeyboardOffsetProvider: NSObject {
12
+ @objc public static let shared = KeyboardOffsetProvider()
13
+
14
+ override private init() {}
15
+
16
+ private var offsetMap: [String: (offset: NSNumber, tag: NSNumber)] = [:]
17
+
18
+ @objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber, withTag tag: NSNumber) {
19
+ KeyboardAreaExtender.shared.updateHeight(to: CGFloat(offset), for: nativeID)
20
+ offsetMap[nativeID] = (offset: offset, tag: tag)
21
+ }
22
+
23
+ @objc public func getOffset(forTextInputNativeID nativeID: String?) -> NSNumber? {
24
+ guard let unwrappedNativeID = nativeID, let pair = offsetMap[unwrappedNativeID] else { return nil }
25
+ return pair.offset
26
+ }
27
+
28
+ @objc public func hasOffset(forTextInputNativeID nativeID: String?) -> Bool {
29
+ guard let unwrappedNativeID = nativeID else { return false }
30
+ return offsetMap[unwrappedNativeID] != nil
31
+ }
32
+
33
+ @objc public func removeOffset(forTextInputNativeID nativeID: String, withTag tag: NSNumber) {
34
+ // Ensure the tag matches before removing the entry
35
+ if let currentPair = offsetMap[nativeID], currentPair.tag == tag {
36
+ offsetMap.removeValue(forKey: nativeID)
37
+ }
38
+ }
39
+ }
@@ -37,7 +37,7 @@ public class FocusedInputObserver: NSObject {
37
37
  // input tracking
38
38
  private var currentInput: UIView?
39
39
  private var currentResponder: UIView?
40
- private var hasObservers = false
40
+ private var observation: NSKeyValueObservation?
41
41
  private var lastEventDispatched: [AnyHashable: Any] = noFocusedInputEvent
42
42
 
43
43
  @objc public init(
@@ -76,7 +76,7 @@ public class FocusedInputObserver: NSObject {
76
76
 
77
77
  NotificationCenter.default.addObserver(
78
78
  self,
79
- selector: #selector(keyboardWillShow),
79
+ selector: #selector(didReceiveFocus),
80
80
  name: UIResponder.keyboardWillShowNotification,
81
81
  object: nil
82
82
  )
@@ -86,6 +86,30 @@ public class FocusedInputObserver: NSObject {
86
86
  name: UIResponder.keyboardWillHideNotification,
87
87
  object: nil
88
88
  )
89
+ NotificationCenter.default.addObserver(
90
+ self,
91
+ selector: #selector(didReceiveFocus),
92
+ name: UITextField.textDidBeginEditingNotification,
93
+ object: nil
94
+ )
95
+ NotificationCenter.default.addObserver(
96
+ self,
97
+ selector: #selector(didReceiveFocus),
98
+ name: UITextView.textDidBeginEditingNotification,
99
+ object: nil
100
+ )
101
+ NotificationCenter.default.addObserver(
102
+ self,
103
+ selector: #selector(didReceiveBlur),
104
+ name: UITextField.textDidEndEditingNotification,
105
+ object: nil
106
+ )
107
+ NotificationCenter.default.addObserver(
108
+ self,
109
+ selector: #selector(didReceiveBlur),
110
+ name: UITextView.textDidEndEditingNotification,
111
+ object: nil
112
+ )
89
113
  }
90
114
 
91
115
  @objc public func unmount() {
@@ -94,7 +118,41 @@ public class FocusedInputObserver: NSObject {
94
118
  NotificationCenter.default.removeObserver(self)
95
119
  }
96
120
 
97
- @objc func keyboardWillShow(_: Notification) {
121
+ @objc func keyboardWillHide(_: Notification) {
122
+ onBlur()
123
+ }
124
+
125
+ @objc func didReceiveFocus(_: Notification) {
126
+ if UIResponder.current == currentResponder {
127
+ // focus was already handled by keyboard event
128
+ return
129
+ }
130
+
131
+ onFocus()
132
+ }
133
+
134
+ /**
135
+ * We handle blur events in `keyboardWillHide` handler
136
+ * And this additional handler is needed only to have
137
+ * a consistent state when keyboard is not shown
138
+ */
139
+ @objc func didReceiveBlur(_: Notification) {
140
+ if currentResponder == nil {
141
+ // blur was already handled by keyboard event
142
+ return
143
+ }
144
+ // blur gets triggered on endEditing, so we check if no upcoming
145
+ // didReceiveFocus events are coming to exclude `noFocusedInput`
146
+ // event when user switches between inputs
147
+ DispatchQueue.main.async {
148
+ // check that it wasn't a switch between inputs
149
+ if self.currentResponder == nil {
150
+ self.onBlur()
151
+ }
152
+ }
153
+ }
154
+
155
+ func onFocus() {
98
156
  guard let responder = UIResponder.current as? UIView else { return }
99
157
  removeObservers(newResponder: responder)
100
158
  currentResponder = responder
@@ -106,7 +164,7 @@ public class FocusedInputObserver: NSObject {
106
164
  FocusedInputHolder.shared.set(currentResponder as? TextInput)
107
165
 
108
166
  let allInputFields = ViewHierarchyNavigator.getAllInputFields()
109
- let currentIndex = allInputFields.firstIndex(where: { $0 as? UIView == currentResponder }) ?? -1
167
+ let currentIndex = allInputFields.firstIndex(where: { $0 == currentResponder }) ?? -1
110
168
 
111
169
  onFocusDidSet([
112
170
  "current": currentIndex,
@@ -114,7 +172,11 @@ public class FocusedInputObserver: NSObject {
114
172
  ])
115
173
  }
116
174
 
117
- @objc func keyboardWillHide(_: Notification) {
175
+ func onBlur() {
176
+ // when we switch to next input, but `showSoftInput={false}`
177
+ if UIResponder.current is TextInput {
178
+ return
179
+ }
118
180
  removeObservers(newResponder: nil)
119
181
  currentInput = nil
120
182
  currentResponder = nil
@@ -162,25 +224,27 @@ public class FocusedInputObserver: NSObject {
162
224
  }
163
225
 
164
226
  private func setupObservers() {
165
- if hasObservers {
227
+ if observation != nil {
166
228
  return
167
229
  }
168
230
 
169
231
  if currentInput != nil {
170
- hasObservers = true
171
- currentInput?.addObserver(self, forKeyPath: "center", options: .new, context: nil)
232
+ observation = currentInput?.observe(\.center, options: .new) { [weak self] view, change in
233
+ guard let self = self else { return }
234
+ self.onLayoutChange(view: view, change: change)
235
+ }
172
236
 
173
237
  substituteDelegate(currentResponder)
174
238
  }
175
239
  }
176
240
 
177
241
  private func removeObservers(newResponder: UIResponder?) {
178
- if newResponder == currentResponder || !hasObservers {
242
+ if newResponder == currentResponder || observation == nil {
179
243
  return
180
244
  }
181
245
 
182
- hasObservers = false
183
- currentInput?.removeObserver(self, forKeyPath: "center", context: nil)
246
+ observation?.invalidate()
247
+ observation = nil
184
248
 
185
249
  substituteDelegateBack(currentResponder)
186
250
  }
@@ -188,7 +252,7 @@ public class FocusedInputObserver: NSObject {
188
252
  private func substituteDelegate(_ input: UIResponder?) {
189
253
  if let textField = input as? UITextField {
190
254
  if !(textField.delegate is KCTextInputCompositeDelegate) {
191
- delegate.setTextFieldDelegate(delegate: textField.delegate)
255
+ delegate.setTextFieldDelegate(delegate: textField.delegate, textField: textField)
192
256
  textField.delegate = delegate
193
257
  }
194
258
  } else if let textView = input as? UITextView {
@@ -211,19 +275,11 @@ public class FocusedInputObserver: NSObject {
211
275
  }
212
276
  }
213
277
 
214
- // swiftlint:disable:next block_based_kvo
215
- @objc override public func observeValue(
216
- forKeyPath keyPath: String?,
217
- of object: Any?,
218
- change _: [NSKeyValueChangeKey: Any]?,
219
- context _: UnsafeMutableRawPointer?
220
- ) {
221
- if keyPath == "center", object as? NSObject == currentInput {
222
- // we need to read layout in next frame, otherwise we'll get old
223
- // layout values
224
- DispatchQueue.main.async {
225
- self.syncUpLayout()
226
- }
278
+ private func onLayoutChange(view _: UIView, change _: NSKeyValueObservedChange<CGPoint>) {
279
+ // we need to read layout in next frame, otherwise we'll get old
280
+ // layout values
281
+ DispatchQueue.main.async {
282
+ self.syncUpLayout()
227
283
  }
228
284
  }
229
285
 
@@ -0,0 +1,39 @@
1
+ //
2
+ // KeyboardEventsIgnorer.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 24/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ @objc(KeyboardEventsIgnorer)
11
+ public class KeyboardEventsIgnorer: NSObject {
12
+ @objc public static let shared = KeyboardEventsIgnorer()
13
+
14
+ var shouldIgnoreKeyboardEvents = false
15
+
16
+ public var shouldIgnore: Bool {
17
+ return shouldIgnoreKeyboardEvents
18
+ }
19
+
20
+ override init() {
21
+ super.init()
22
+ NotificationCenter.default.addObserver(
23
+ self,
24
+ selector: #selector(handleIgnoreKeyboardEventsNotification),
25
+ name: .shouldIgnoreKeyboardEvents,
26
+ object: nil
27
+ )
28
+ }
29
+
30
+ @objc private func handleIgnoreKeyboardEventsNotification(_ notification: Notification) {
31
+ if let userInfo = notification.userInfo, let value = userInfo["ignore"] as? Bool {
32
+ shouldIgnoreKeyboardEvents = value
33
+ }
34
+ }
35
+
36
+ deinit {
37
+ NotificationCenter.default.removeObserver(self)
38
+ }
39
+ }
@@ -33,10 +33,15 @@ public class KeyboardMovementObserver: NSObject {
33
33
  private var _windowsCount: Int = 0
34
34
  private var prevKeyboardPosition = 0.0
35
35
  private var displayLink: CADisplayLink?
36
- private var hasKVObserver = false
36
+ private var interactiveKeyboardObserver: NSKeyValueObservation?
37
37
  private var isMounted = false
38
38
  // state variables
39
- private var keyboardHeight: CGFloat = 0.0
39
+ private var _keyboardHeight: CGFloat = 0.0
40
+ private var keyboardHeight: CGFloat {
41
+ get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
42
+ set { _keyboardHeight = newValue }
43
+ }
44
+
40
45
  private var duration = 0
41
46
  private var tag: NSNumber = -1
42
47
  private var animation: KeyboardAnimation?
@@ -88,72 +93,57 @@ public class KeyboardMovementObserver: NSObject {
88
93
  }
89
94
 
90
95
  private func setupKVObserver() {
91
- if hasKVObserver {
92
- return
93
- }
96
+ guard interactiveKeyboardObserver == nil, let view = keyboardView else { return }
94
97
 
95
- if keyboardView != nil {
96
- hasKVObserver = true
97
- keyboardView?.addObserver(self, forKeyPath: "center", options: .new, context: nil)
98
+ interactiveKeyboardObserver = view.observe(\.center, options: .new) { [weak self] _, change in
99
+ guard let self = self, let changeValue = change.newValue else { return }
100
+
101
+ self.keyboardDidMoveInteractively(changeValue: changeValue)
98
102
  }
99
103
  }
100
104
 
101
105
  private func removeKVObserver() {
102
- if !hasKVObserver {
106
+ interactiveKeyboardObserver?.invalidate()
107
+ interactiveKeyboardObserver = nil
108
+ }
109
+
110
+ private func keyboardDidMoveInteractively(changeValue: CGPoint) {
111
+ // if we are currently animating keyboard -> we need to ignore values from KVO
112
+ if displayLink != nil {
113
+ return
114
+ }
115
+ // if keyboard height is not equal to its bounds - we can ignore
116
+ // values, since they'll be invalid and will cause UI jumps
117
+ if floor(keyboardView?.bounds.size.height ?? 0) != floor(_keyboardHeight) {
103
118
  return
104
119
  }
105
120
 
106
- hasKVObserver = false
107
- _keyboardView?.removeObserver(self, forKeyPath: "center", context: nil)
108
- }
121
+ let keyboardFrameY = changeValue.y
122
+ let keyboardWindowH = keyboardView?.window?.bounds.size.height ?? 0
123
+ let keyboardPosition = keyboardWindowH - keyboardFrameY
109
124
 
110
- // swiftlint:disable:next block_based_kvo
111
- @objc override public func observeValue(
112
- forKeyPath keyPath: String?,
113
- of object: Any?,
114
- change: [NSKeyValueChangeKey: Any]?,
115
- context _: UnsafeMutableRawPointer?
116
- ) {
117
- if keyPath == "center", object as? NSObject == _keyboardView {
118
- // if we are currently animating keyboard -> we need to ignore values from KVO
119
- if displayLink != nil {
120
- return
121
- }
122
- // if keyboard height is not equal to its bounds - we can ignore
123
- // values, since they'll be invalid and will cause UI jumps
124
- if keyboardView?.bounds.size.height != keyboardHeight {
125
- return
126
- }
125
+ let position = CGFloat.interpolate(
126
+ inputRange: [_keyboardHeight / 2, -_keyboardHeight / 2],
127
+ outputRange: [_keyboardHeight, 0],
128
+ currentValue: keyboardPosition
129
+ ) - KeyboardAreaExtender.shared.offset
127
130
 
128
- guard let changeValue = change?[.newKey] as? NSValue else {
129
- return
130
- }
131
- let keyboardFrameY = changeValue.cgPointValue.y
132
- let keyboardWindowH = keyboardView?.window?.bounds.size.height ?? 0
133
- let keyboardPosition = keyboardWindowH - keyboardFrameY
134
- let position = CGFloat.interpolate(
135
- inputRange: [keyboardHeight / 2, -keyboardHeight / 2],
136
- outputRange: [keyboardHeight, 0],
137
- currentValue: keyboardPosition
138
- )
139
-
140
- if position == 0 {
141
- // it will be triggered before `keyboardWillDisappear` and
142
- // we don't need to trigger `onInteractive` handler for that
143
- // since it will be handled in `keyboardWillDisappear` function
144
- return
145
- }
131
+ if position == 0 {
132
+ // it will be triggered before `keyboardWillDisappear` and
133
+ // we don't need to trigger `onInteractive` handler for that
134
+ // since it will be handled in `keyboardWillDisappear` function
135
+ return
136
+ }
146
137
 
147
- prevKeyboardPosition = position
138
+ prevKeyboardPosition = position
148
139
 
149
- onEvent(
150
- "onKeyboardMoveInteractive",
151
- position as NSNumber,
152
- position / CGFloat(keyboardHeight) as NSNumber,
153
- -1,
154
- tag
155
- )
156
- }
140
+ onEvent(
141
+ "onKeyboardMoveInteractive",
142
+ position as NSNumber,
143
+ position / CGFloat(keyboardHeight) as NSNumber,
144
+ -1,
145
+ tag
146
+ )
157
147
  }
158
148
 
159
149
  @objc public func unmount() {
@@ -163,6 +153,8 @@ public class KeyboardMovementObserver: NSObject {
163
153
  }
164
154
 
165
155
  @objc func keyboardWillAppear(_ notification: Notification) {
156
+ guard !KeyboardEventsIgnorer.shared.shouldIgnore else { return }
157
+
166
158
  let (duration, frame) = notification.keyboardMetaData()
167
159
  if let keyboardFrame = frame {
168
160
  tag = UIResponder.current.reactViewTag
@@ -172,11 +164,11 @@ public class KeyboardMovementObserver: NSObject {
172
164
  didShowDeadline = Date.currentTimeStamp + Int64(duration)
173
165
 
174
166
  onRequestAnimation()
175
- onEvent("onKeyboardMoveStart", Float(keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
176
- onNotify("KeyboardController::keyboardWillShow", buildEventParams(keyboardHeight, duration, tag))
167
+ onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
168
+ onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
177
169
 
178
170
  setupKeyboardWatcher()
179
- initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
171
+ initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
180
172
  }
181
173
  }
182
174
 
@@ -202,9 +194,15 @@ public class KeyboardMovementObserver: NSObject {
202
194
  let keyboardHeight = keyboardFrame.cgRectValue.size.height
203
195
  tag = UIResponder.current.reactViewTag
204
196
  self.keyboardHeight = keyboardHeight
197
+
198
+ guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
199
+ KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
200
+ return
201
+ }
202
+
205
203
  // if the event is caught in between it's highly likely that it could be a "resize" event
206
204
  // so we just read actual keyboard frame value in this case
207
- let height = timestamp >= didShowDeadline ? keyboardHeight : position
205
+ let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
208
206
  // always limit progress to the maximum possible value
209
207
  let progress = min(height / self.keyboardHeight, 1.0)
210
208
 
@@ -267,7 +265,7 @@ public class KeyboardMovementObserver: NSObject {
267
265
  }
268
266
 
269
267
  let (visibleKeyboardHeight, keyboardFrameY) = keyboardView.frameTransitionInWindow
270
- var keyboardPosition = visibleKeyboardHeight
268
+ var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
271
269
 
272
270
  if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
273
271
  return
@@ -11,9 +11,11 @@ import UIKit
11
11
 
12
12
  public protocol TextInput: UIView {
13
13
  // default common methods/properties
14
+ var inputAccessoryView: UIView? { get set }
14
15
  var inputView: UIView? { get set }
15
16
  var keyboardType: UIKeyboardType { get }
16
17
  var keyboardAppearance: UIKeyboardAppearance { get }
18
+ // custom methods/properties
17
19
  func focus()
18
20
  }
19
21
 
@@ -0,0 +1,60 @@
1
+ //
2
+ // UIResponderSwizzle.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ private var originalResignFirstResponder: IMP?
12
+
13
+ @objc
14
+ extension UIResponder {
15
+ public static func swizzleResignFirstResponder() {
16
+ let originalSelector = #selector(resignFirstResponder)
17
+
18
+ guard let originalMethod = class_getInstanceMethod(UIResponder.self, originalSelector) else {
19
+ return
20
+ }
21
+
22
+ originalResignFirstResponder = method_getImplementation(originalMethod)
23
+
24
+ let swizzledImplementation: @convention(block) (UIResponder) -> Bool = { (self) in
25
+ // Check the type of responder
26
+ if let textField = self as? TextInput {
27
+ // check inputAccessoryView and call original method immediately if not InvisibleInputAccessoryView
28
+ if !(textField.inputAccessoryView is InvisibleInputAccessoryView) {
29
+ return self.callOriginalResignFirstResponder(originalSelector)
30
+ }
31
+ } else {
32
+ // If casting to TextInput fails
33
+ return self.callOriginalResignFirstResponder(originalSelector)
34
+ }
35
+
36
+ KeyboardAreaExtender.shared.hide()
37
+
38
+ // Postpone execution of the original resignFirstResponder
39
+ DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
40
+ (self as? TextInput)?.inputAccessoryView = nil
41
+ KeyboardAreaExtender.shared.remove()
42
+ _ = self.callOriginalResignFirstResponder(originalSelector)
43
+ }
44
+
45
+ // We need to return a value immediately, even though the actual action is delayed
46
+ return false
47
+ }
48
+
49
+ let implementation = imp_implementationWithBlock(swizzledImplementation)
50
+ method_setImplementation(originalMethod, implementation)
51
+ }
52
+
53
+ private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool {
54
+ guard let originalResignFirstResponder = originalResignFirstResponder else { return false }
55
+ typealias Function = @convention(c) (AnyObject, Selector) -> Bool
56
+ let castOriginalResignFirstResponder = unsafeBitCast(originalResignFirstResponder, to: Function.self)
57
+ let result = castOriginalResignFirstResponder(self, selector)
58
+ return result
59
+ }
60
+ }
@@ -21,7 +21,7 @@ public class ViewHierarchyNavigator: NSObject {
21
21
  return
22
22
  }
23
23
 
24
- let input = (FocusedInputHolder.shared.get() as? UIView) ?? (UIResponder.current as? UIView) ?? nil
24
+ let input = FocusedInputHolder.shared.get() ?? (UIResponder.current as? UIView) ?? nil
25
25
  guard let view = input else { return }
26
26
 
27
27
  let textField = findTextInputInDirection(currentFocus: view, direction: direction)