Skip to content

Commit 99be9d3

Browse files
authored
fix: avoid ensureLayout calls (#1174)
## 📜 Description Avoid `ensureLayout` calls. ## 💡 Motivation and Context The issue stems from the fact that `ensureLayout` changes internal state of the input, and when react-native changes something internally it relies that certain state should be present, but `ensureLayout` changes this state, so as a result it leads to crash described in #1173 I've been thinking a lot on how to fix this issue, and the only way I found (why exactly the issue happens) is because JS updates input of the state: ```objc if (_mostRecentEventCount == _state->getData().mostRecentEventCount) { _comingFromJS = YES; [self _setAttributedString:RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox)]; _comingFromJS = NO; } ``` If we look further in the code we can find that this flag filters-out undesired events: ```objc - (void)textInputDidChange { if (_comingFromJS) { return; } ``` and ```objc - (void)textInputDidChangeSelection { if (_comingFromJS) { return; } ``` So I decided to use the same approach and ignore events if `_comingFromJS` is `true`. Fortunately I have an access to this through `textInputDelegate` field (and the n I can easily get an access to `_comingFromJS`. This field is private so I had to use `value(forKey`. On paper architecture this field is not available (it doesn't exist) so direct calls to `value(forKey` will lead to crash. I've tried to use the same approach as in #785 but `responds` in this case will return `false` and I'll never get `_isComingFromJS`. To overcome this problem I created small objc extensions that simply tries to get a field and in case of exception returns `nil` (we can catch exception in objc, but that exception is not propagated to swift code, so in swift case the app will simply crash). And since I created a safe version of `value(forKey` I also decided to get rid off `responds` check in `nativeId` querying and rewrite it to `safeValue(forKey`. Checked that #785 and #1148 are not reproducible Closes #1173 #1168 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### iOS - don't use `ensureLayout` method; - added `_safeValue` method in ObjC and `safeValue` extension in Swift; - use new `safeValue` method (instead of `responds` + access); - return `false` from `canSelectionFitIntoLayout` if `_comingFromJS` is `true`. ## 🤔 How Has This Been Tested? Tested on iPhone 16 Pro (iOS 26). ## 📸 Screenshots (if appropriate): https://github.com/user-attachments/assets/2b2b42c8-cd3e-4f97-ad06-9b04f21ddca1 ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent 52b6cdb commit 99be9d3

5 files changed

Lines changed: 51 additions & 9 deletions

File tree

ios/extensions/NSObject.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// NSObject.swift
3+
// KeyboardController
4+
//
5+
// Created by Kiryl Ziusko on 26/10/2025.
6+
//
7+
8+
public extension NSObject {
9+
func safeValue(forKey key: String) -> Any? {
10+
return _safeValue(forKey: key)
11+
}
12+
}

ios/extensions/UIResponder.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ public extension Optional where Wrapped == UIResponder {
3030
guard let superview = (self as? UIView)?.superview else { return nil }
3131

3232
#if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
33-
if superview.responds(to: Selector(("nativeId"))) == true {
34-
return (superview as NSObject).value(forKey: "nativeId") as? String
35-
}
36-
37-
return nil
33+
return superview.safeValue(forKey: "nativeId") as? String
3834
#else
3935
return superview.nativeID
4036
#endif

ios/extensions/UITextInput.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import UIKit
1111

1212
public extension UITextInput {
1313
var canSelectionFitIntoLayout: Bool {
14-
guard let textView = self as? UITextView else { return true }
15-
16-
// Force layout to ensure accurate rect calculation
17-
textView.layoutManager.ensureLayout(for: textView.textContainer)
14+
if let selfObj = self as? NSObject,
15+
let delegate = selfObj.safeValue(forKey: "textInputDelegate") as? NSObject,
16+
let comingFromJS = delegate.safeValue(forKey: "_comingFromJS") as? Bool,
17+
comingFromJS
18+
{
19+
return false
20+
}
1821

1922
guard let selectedRange = selectedTextRange else { return false }
2023

ios/objc/NSObject+SafeKVC.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// NSObject+SafeKVC.h
3+
// KeyboardController
4+
//
5+
// Created by Kiryl Ziusko on 26/10/2025.
6+
//
7+
8+
#import <Foundation/Foundation.h>
9+
10+
@interface NSObject (SafeKVC)
11+
- (id)_safeValueForKey:(NSString *)key;
12+
@end

ios/objc/NSObject+SafeKVC.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// NSObject+SafeKVC.m
3+
// KeyboardController
4+
//
5+
// Created by Kiryl Ziusko on 26/10/2025.
6+
//
7+
8+
#import "NSObject+SafeKVC.h"
9+
10+
@implementation NSObject (SafeKVC)
11+
- (id)_safeValueForKey:(NSString *)key
12+
{
13+
@try {
14+
return [self valueForKey:key];
15+
} @catch (NSException *exception) {
16+
return nil;
17+
}
18+
}
19+
@end

0 commit comments

Comments
 (0)