react-native-keyboard-controller 1.21.12 → 1.21.13

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.
@@ -106,7 +106,7 @@ class FocusedInputObserver(
106
106
  // remove it asynchronously to avoid crash in stripe input
107
107
  // see https://github.com/stripe/stripe-android/issues/10178
108
108
  input.post {
109
- input.removeTextChangedListener(watcher)
109
+ watcher?.let { input.removeTextChangedListener(it) }
110
110
  }
111
111
  }
112
112
  selectionSubscription?.invoke()
@@ -0,0 +1,16 @@
1
+ //
2
+ // KeyboardControllerConfiguration.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 25/06/2026.
6
+ //
7
+
8
+ enum KeyboardControllerConfiguration {
9
+ static var usesKeyboardLayoutGuideTracking: Bool {
10
+ if #available(iOS 26.0, *) {
11
+ return true
12
+ }
13
+
14
+ return false
15
+ }
16
+ }
@@ -65,7 +65,7 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
65
65
  // MARK: setters/getters
66
66
 
67
67
  func setTextViewDelegate(delegate: UITextViewDelegate?) {
68
- // remove KVO from any old textView
68
+ // remove KVO from any old textField
69
69
  if let oldTextField = observedTextFieldForSelection {
70
70
  removeSelectionRangeObserver(from: oldTextField)
71
71
  }
@@ -96,21 +96,6 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
96
96
  }
97
97
  }
98
98
 
99
- func canSubstituteTextFieldDelegate(delegate: UITextFieldDelegate?) -> Bool {
100
- let type = String(describing: delegate)
101
- if type.range(of: "SQIPTextFieldInputModifier") != nil {
102
- // SQIPTextFieldInputModifier is a private class used internally by Square.
103
- // It forwards input events to the keyboard-controller delegate.
104
- // To prevent an infinite loop, we avoid setting our delegate in this case.
105
- // Since Square's SDK is used imperatively and doesn't allow adding custom components,
106
- // keyboard-controller components cannot be used in this context,
107
- // so it's safe to skip replacing the delegate.
108
- return false
109
- }
110
-
111
- return true
112
- }
113
-
114
99
  /// Getter for the active delegate
115
100
  var activeDelegate: AnyObject? {
116
101
  return textViewDelegate ?? textFieldDelegate
@@ -208,15 +193,49 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
208
193
 
209
194
  // MARK: call forwarding
210
195
 
196
+ /// Decides whether `aSelector` should be forwarded to `delegate`.
197
+ ///
198
+ /// Usually we stay fully transparent and forward every selector the
199
+ /// underlying delegate responds to. The exception is a field that acts as its
200
+ /// own UIKit delegate and relays callbacks to an inner delegate through
201
+ /// `forwardInvocation:` (e.g. `BKForwardingTextField`). For such a field
202
+ /// `activeDelegate` is the text input *itself*, so it also "responds" to the
203
+ /// many `UITextField` / `UITextView` methods that UIKit relays to its delegate
204
+ /// (e.g. `keyboardInputChangedSelection:`). Forwarding one of those back to the
205
+ /// field makes the field relay it to us again — an infinite recursion.
206
+ ///
207
+ /// To stay transparent without looping, when the delegate is the text input
208
+ /// itself we forward only the selectors it handles *dynamically* (i.e. the
209
+ /// ones it routes to its inner delegate via its own message forwarding), and
210
+ /// never the ones it implements as a UIKit object. `instancesRespond(to:)`
211
+ /// inspects the real method table only, so it is `false` exactly for the
212
+ /// selectors resolved through the field's own `forwardInvocation:`.
213
+ private func shouldForward(_ aSelector: Selector!, to delegate: AnyObject) -> Bool {
214
+ guard delegate.responds(to: aSelector) else {
215
+ return false
216
+ }
217
+
218
+ if delegate is UITextField || delegate is UITextView,
219
+ let delegateClass = type(of: delegate) as? NSObject.Type
220
+ {
221
+ return !delegateClass.instancesRespond(to: aSelector)
222
+ }
223
+
224
+ return true
225
+ }
226
+
211
227
  override func responds(to aSelector: Selector!) -> Bool {
212
228
  if super.responds(to: aSelector) {
213
229
  return true
214
230
  }
215
- return activeDelegate?.responds(to: aSelector) ?? false
231
+ guard let activeDelegate = activeDelegate else {
232
+ return false
233
+ }
234
+ return shouldForward(aSelector, to: activeDelegate)
216
235
  }
217
236
 
218
237
  override func forwardingTarget(for aSelector: Selector!) -> Any? {
219
- if activeDelegate?.responds(to: aSelector) ?? false {
238
+ if let activeDelegate = activeDelegate, shouldForward(aSelector, to: activeDelegate) {
220
239
  return activeDelegate
221
240
  }
222
241
  return super.forwardingTarget(for: aSelector)
@@ -1,5 +1,5 @@
1
1
  //
2
- // UITextView+DelegateManager.h
2
+ // TextInput+DelegateManager.h
3
3
  // react-native-keyboard-controller
4
4
  //
5
5
  // Created by Kiryl Ziusko on 24/04/2024.
@@ -10,3 +10,9 @@
10
10
  - (void)setForceDelegate:(id<UITextViewDelegate>)delegate;
11
11
 
12
12
  @end
13
+
14
+ @interface UITextField (DelegateManager)
15
+
16
+ - (void)setForceDelegate:(id<UITextFieldDelegate>)delegate;
17
+
18
+ @end
@@ -0,0 +1,55 @@
1
+ //
2
+ // TextInput+DelegateManager.m
3
+ // react-native-keyboard-controller
4
+ //
5
+ // Created by Kiryl Ziusko on 24/04/2024.
6
+ //
7
+
8
+ #import <objc/runtime.h>
9
+ #import "TextInput+DelegateManager.h"
10
+
11
+ @implementation UITextView (DelegateManager)
12
+
13
+ /**
14
+ * We are aware that delegate is set by RN on mount and we can not re-define it
15
+ * but we are using composite delegate to forward all calls to original delegate
16
+ * so we use this method to force set our delegate
17
+ */
18
+ - (void)setForceDelegate:(id<UITextViewDelegate>)delegate
19
+ {
20
+ Ivar ivar = class_getInstanceVariable([UITextView class], "_delegate");
21
+ if (ivar) {
22
+ object_setIvar(self, ivar, delegate);
23
+ }
24
+ }
25
+
26
+ @end
27
+
28
+ @implementation UITextField (DelegateManager)
29
+
30
+ /**
31
+ * Some `UITextField` subclasses (e.g. card-input fields from third-party SDKs, aka Square)
32
+ * make themselves their own UIKit delegate via `[super setDelegate:self]` and
33
+ * override `setDelegate:` so that the public setter only stores an *inner*
34
+ * delegate, forwarding UIKit callbacks to it via `forwardInvocation:`.
35
+ *
36
+ * For such fields the regular `textField.delegate = composite` does NOT replace
37
+ * the real UIKit delegate slot — it only points the field's inner delegate at us
38
+ * while the field itself stays the real delegate. Combined with the composite
39
+ * forwarding unhandled calls back to that same field, this creates an infinite
40
+ * recursion (it blows the stack already inside `respondsToSelector:`).
41
+ *
42
+ * Writing the `_delegate` ivar directly installs the composite as the genuine
43
+ * UIKit delegate without touching the field's inner delegate slot, so no
44
+ * back-pointer to the composite is ever created and the chain stays acyclic:
45
+ * `UIKit -> composite -> field -> field's original inner delegate`.
46
+ */
47
+ - (void)setForceDelegate:(id<UITextFieldDelegate>)delegate
48
+ {
49
+ Ivar ivar = class_getInstanceVariable([UITextField class], "_delegate");
50
+ if (ivar) {
51
+ object_setIvar(self, ivar, delegate);
52
+ }
53
+ }
54
+
55
+ @end
@@ -251,11 +251,9 @@ public class FocusedInputObserver: NSObject {
251
251
 
252
252
  private func substituteDelegate(_ input: UIResponder?) {
253
253
  if let textField = input as? UITextField {
254
- if !(textField.delegate is KCTextInputCompositeDelegate),
255
- delegate.canSubstituteTextFieldDelegate(delegate: textField.delegate)
256
- {
254
+ if !(textField.delegate is KCTextInputCompositeDelegate) {
257
255
  delegate.setTextFieldDelegate(delegate: textField.delegate, textField: textField)
258
- textField.delegate = delegate
256
+ textField.setForceDelegate(delegate)
259
257
  }
260
258
  } else if let textView = input as? UITextView {
261
259
  if !(textView.delegate is KCTextInputCompositeDelegate) {
@@ -267,7 +265,7 @@ public class FocusedInputObserver: NSObject {
267
265
 
268
266
  private func substituteDelegateBack(_ input: UIResponder?) {
269
267
  if let textField = input as? UITextField, let oldDelegate = delegate.activeDelegate as? UITextFieldDelegate {
270
- textField.delegate = oldDelegate
268
+ textField.setForceDelegate(oldDelegate)
271
269
  } else if let textView = input as? UITextView, let oldDelegate = delegate.activeDelegate as? UITextViewDelegate {
272
270
  textView.setForceDelegate(oldDelegate)
273
271
  }
@@ -8,8 +8,8 @@
8
8
  import UIKit
9
9
 
10
10
  /**
11
- * A compatibility view that resolves to `KeyboardView` on iOS < 26
12
- * and uses `keyboardLayoutGuide` on iOS 26+.
11
+ * A compatibility view that resolves to `KeyboardView` for the legacy path
12
+ * and uses `keyboardLayoutGuide` for configured iOS 26+ behavior.
13
13
  */
14
14
  public final class KeyboardTrackingView: UIView {
15
15
  private var keyboardView: UIView? {
@@ -69,6 +69,8 @@ public final class KeyboardTrackingView: UIView {
69
69
  }
70
70
 
71
71
  @objc public func attachToTopmostView(toWindow window: UIWindow? = nil) {
72
+ guard KeyboardControllerConfiguration.usesKeyboardLayoutGuideTracking else { return }
73
+
72
74
  var topViewController = window?.rootViewController
73
75
  if let rootVC = topViewController, let topView = rootVC.view, topView.window != nil {
74
76
  // ok, attach
@@ -120,7 +122,7 @@ public final class KeyboardTrackingView: UIView {
120
122
  }
121
123
 
122
124
  @objc var view: UIView? {
123
- if #available(iOS 26.0, *) {
125
+ if KeyboardControllerConfiguration.usesKeyboardLayoutGuideTracking {
124
126
  return self
125
127
  } else {
126
128
  return keyboardView
@@ -135,7 +137,7 @@ public final class KeyboardTrackingView: UIView {
135
137
  let keyboardPosition = keyboardWindowH - keyboardFrameY
136
138
 
137
139
  // for `keyboardLayoutGuide` case we can just read keyboard position directly - no interpolation needed
138
- if #available(iOS 26.0, *) {
140
+ if KeyboardControllerConfiguration.usesKeyboardLayoutGuideTracking {
139
141
  if keyboardPosition > keyboardHeight {
140
142
  return Self.invalidPosition
141
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.21.12",
3
+ "version": "1.21.13",
4
4
  "description": "Keyboard manager which works in identical way on both iOS and Android",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -1,26 +0,0 @@
1
- //
2
- // UITextView+DelegateManager.m
3
- // react-native-keyboard-controller
4
- //
5
- // Created by Kiryl Ziusko on 24/04/2024.
6
- //
7
-
8
- #import <objc/runtime.h>
9
- #import "UITextView+DelegateManager.h"
10
-
11
- @implementation UITextView (DelegateManager)
12
-
13
- /**
14
- * We are aware that delegate is set by RN on mount and we can not re-define it
15
- * but we are using composite delegate to forward all calls to original delegate
16
- * so we use this method to force set our delegate
17
- */
18
- - (void)setForceDelegate:(id<UITextViewDelegate>)delegate
19
- {
20
- Ivar ivar = class_getInstanceVariable([UITextView class], "_delegate");
21
- if (ivar) {
22
- object_setIvar(self, ivar, delegate);
23
- }
24
- }
25
-
26
- @end