diff --git a/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.mm b/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.mm index 9befda4..c9e6557 100644 --- a/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.mm +++ b/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.mm @@ -12,10 +12,13 @@ #import "RNCEKVFocusProtocol.h" @implementation RNCEKVFocusDelegate{ - UIView* _delegate; + __weak UIView* _delegate; // The view UIKit actually focused inside our subtree (set from the focus engine, // not guessed). Weak so a removed/recycled view can't be retained or go stale. __weak UIView* _focusedTarget; + // Tracks wrapper focus after the weak target is deallocated, allowing the + // next focus update to report blur. + BOOL _isTrackingFocus; } - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)delegate{ @@ -28,6 +31,7 @@ - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)de - (void)reset { _focusedTarget = nil; + _isTrackingFocus = NO; } // Whether `view` is the focus target THIS wrapper owns. A non-wrapper owns only @@ -119,18 +123,20 @@ - (NSNumber*)isFocusChanged:(UIFocusUpdateContext *)context { UIView *next = context.nextFocusedView; UIView *prev = context.previouslyFocusedView; - // Focus entered our subtree: remember the *actual* focused view and report focus. + // Track the actual focused view. Moving between this wrapper's descendants + // updates the target without reporting another focus event. if (next && [self ownsFocusedView:next]) { - if (next == _focusedTarget) { - return nil; // already tracking this view — not a change - } + BOOL alreadyFocused = _isTrackingFocus; _focusedTarget = next; - return @YES; + _isTrackingFocus = YES; + return alreadyFocused ? nil : @YES; } - // Focus left the view we were tracking. - if (prev && prev == _focusedTarget) { + // Report blur when focus leaves the tracked view, including after the weak + // target has been deallocated. + if (_isTrackingFocus && (_focusedTarget == nil || prev == _focusedTarget)) { _focusedTarget = nil; + _isTrackingFocus = NO; return @NO; } diff --git a/ios/Delegates/RNCEKVFocusLinkDelegate/RNCEKVFocusLinkDelegate.mm b/ios/Delegates/RNCEKVFocusLinkDelegate/RNCEKVFocusLinkDelegate.mm index 0d17806..a9aede7 100644 --- a/ios/Delegates/RNCEKVFocusLinkDelegate/RNCEKVFocusLinkDelegate.mm +++ b/ios/Delegates/RNCEKVFocusLinkDelegate/RNCEKVFocusLinkDelegate.mm @@ -17,7 +17,7 @@ @implementation RNCEKVFocusLinkDelegate { BOOL _isFocused; - UIView *_delegate; + __weak UIView *_delegate; NSMutableDictionary *_sides; NSMutableDictionary *_subscribers; } diff --git a/ios/Delegates/RNCEKVFocusSequenceDelegate/RNCEKVFocusSequenceDelegate.mm b/ios/Delegates/RNCEKVFocusSequenceDelegate/RNCEKVFocusSequenceDelegate.mm index acc9894..81bbdc1 100644 --- a/ios/Delegates/RNCEKVFocusSequenceDelegate/RNCEKVFocusSequenceDelegate.mm +++ b/ios/Delegates/RNCEKVFocusSequenceDelegate/RNCEKVFocusSequenceDelegate.mm @@ -8,7 +8,7 @@ #import "RNCEKVOrderLinking.h" #import "RNCEKVOrderRelationship.h" #import "RNCEKVKeyboardFocusableProtocol.h" -#import "UIViewController+RNCEKVExternalKeyboard.h" +#import "RNCEKVKeyboardFocusService.h" #import "UIView+React.h" static NSNumber *const FOCUS_DEFAULT = nil; @@ -16,7 +16,7 @@ @implementation RNCEKVFocusSequenceDelegate { BOOL _isLinked; - UIView *_delegate; + __weak UIView *_delegate; } - (instancetype)initWithView:(UIView *)delegate { @@ -65,10 +65,7 @@ - (void)keyboardedViewFocus:(UIView *)view { } - (void)defaultViewFocus:(UIView *)view { - UIViewController *controller = _delegate.reactViewController; - if (controller != nil) { - [controller rncekvFocusView:view]; - } + [RNCEKVKeyboardFocusService focus:view withFallback:_delegate.reactViewController]; } #pragma mark - Sequential navigation @@ -81,7 +78,7 @@ - (BOOL)handleNextFocus:(UIView *)current if (entry == current) { [self keyboardedViewFocus:[orderRelationship getItem:0]]; - return NO; + return YES; } if (currentIndex == orderRelationship.count - 1 && exit) { @@ -135,6 +132,13 @@ - (NSNumber *)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context { return FOCUS_DEFAULT; } + if (orderRelationship.entry != nil && orderRelationship.entry.window == nil) { + orderRelationship.entry = nil; + } + if (orderRelationship.exit != nil && orderRelationship.exit.window == nil) { + orderRelationship.exit = nil; + } + int currentIndex = [orderRelationship getItemIndex:current]; int nextIndex = [orderRelationship getItemIndex:next]; diff --git a/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierDelegate.mm b/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierDelegate.mm index 1aecd5b..ad19602 100644 --- a/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierDelegate.mm +++ b/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierDelegate.mm @@ -15,7 +15,7 @@ @implementation RNCEKVGroupIdentifierDelegate { - UIView* _delegate; + __weak UIView* _delegate; NSString* _tagId; } diff --git a/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.h b/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.h index 2a83ee8..0e20579 100644 --- a/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.h +++ b/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.h @@ -11,7 +11,7 @@ #import @interface UIViewController (RNCEKVExternalKeyboard) -@property (nonatomic, strong) UIView *rncekvCustomFocusView; +@property (nonatomic, weak) UIView *rncekvCustomFocusView; - (void)rncekvFocusView:(UIView *)view; @end diff --git a/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.mm b/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.mm index d37b6cc..c0f852b 100644 --- a/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.mm +++ b/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.mm @@ -21,16 +21,29 @@ static void RNCEKVUIViewControllerSwizzle(void) { RNCEKVSwizzleInstanceMethod([UIViewController class], @selector(preferredFocusEnvironments), @selector(keyboardedPreferredFocusEnvironments)); } +@interface RNCEKVWeakFocusViewHolder : NSObject +@property (nonatomic, weak) UIView *view; +@end + +@implementation RNCEKVWeakFocusViewHolder +@end + @implementation UIViewController (RNCEKVExternalKeyboard) RNCEKV_INSTALL_SWIZZLES(RNCEKVUIViewControllerSwizzle) - (UIView *)rncekvCustomFocusView { - return objc_getAssociatedObject(self, &kCustomFocusViewKey); + RNCEKVWeakFocusViewHolder *holder = objc_getAssociatedObject(self, &kCustomFocusViewKey); + return holder.view; } - (void)setRncekvCustomFocusView:(UIView *)customFocusView { - objc_setAssociatedObject(self, &kCustomFocusViewKey, customFocusView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + RNCEKVWeakFocusViewHolder *holder = nil; + if (customFocusView != nil) { + holder = [RNCEKVWeakFocusViewHolder new]; + holder.view = customFocusView; + } + objc_setAssociatedObject(self, &kCustomFocusViewKey, holder, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)keyboardedViewDidAppear:(BOOL)animated { @@ -49,13 +62,19 @@ - (void)rncekvFocusView:(UIView *)view { - (NSArray> *)keyboardedPreferredFocusEnvironments { NSArray> *originalEnvironments = [self keyboardedPreferredFocusEnvironments]; - NSMutableArray *focusEnvironments = [originalEnvironments mutableCopy]; + RNCEKVWeakFocusViewHolder *holder = objc_getAssociatedObject(self, &kCustomFocusViewKey); + if (holder == nil) { + return originalEnvironments; + } - UIView *customFocusView = self.rncekvCustomFocusView; - if (customFocusView) { - [focusEnvironments insertObject:customFocusView atIndex:0]; + UIView *customFocusView = holder.view; + if (customFocusView == nil || customFocusView.window == nil) { + self.rncekvCustomFocusView = nil; + return originalEnvironments; } + NSMutableArray *focusEnvironments = [originalEnvironments mutableCopy]; + [focusEnvironments insertObject:customFocusView atIndex:0]; return focusEnvironments; } diff --git a/ios/Services/RNCEKVKeyboardFocusService.h b/ios/Services/RNCEKVKeyboardFocusService.h index e1a548f..7b233ef 100644 --- a/ios/Services/RNCEKVKeyboardFocusService.h +++ b/ios/Services/RNCEKVKeyboardFocusService.h @@ -23,6 +23,12 @@ /// Moves keyboard focus to the given view on the next focus update. + (void)focus:(UIView *)view; +/// Routes focus through the target's window root when available, keeping the +/// request in the target's scene. For an unattached target, falls back to the +/// key-window root and then `controller`. Returns the controller that received +/// the request, or nil if the request could not be routed. ++ (UIViewController *)focus:(UIView *)view withFallback:(UIViewController *)controller; + @end #endif /* RNCEKVKeyboardFocusService_h */ diff --git a/ios/Services/RNCEKVKeyboardFocusService.mm b/ios/Services/RNCEKVKeyboardFocusService.mm index efd038f..5f80725 100644 --- a/ios/Services/RNCEKVKeyboardFocusService.mm +++ b/ios/Services/RNCEKVKeyboardFocusService.mm @@ -41,14 +41,19 @@ + (void)updatePreferredFocusEnvironment:(UIView *)view { } + (void)focus:(UIView *)view { + [self focus:view withFallback:nil]; +} + ++ (UIViewController *)focus:(UIView *)view withFallback:(UIViewController *)controller { if (!view) { - return; + return nil; } - UIWindow *window = RCTKeyWindow(); - if (window && window.rootViewController) { - [window.rootViewController rncekvFocusView:view]; - } + UIViewController *targetController = view.window.rootViewController + ?: RCTKeyWindow().rootViewController + ?: controller; + [targetController rncekvFocusView:view]; + return targetController; } @end diff --git a/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.h b/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.h index b6443b0..2d427cf 100644 --- a/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.h +++ b/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.h @@ -10,8 +10,8 @@ @interface RNCEKVOrderRelationship : NSObject -@property UIView* entry; -@property UIView* exit; +@property (weak) UIView* entry; +@property (weak) UIView* exit; - (void)add:(NSNumber*)position withObject:(NSObject*)obj; - (void)remove:(NSNumber*)position; diff --git a/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.mm b/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.mm index d041114..0643b78 100644 --- a/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.mm +++ b/ios/Services/RNCEKVKeyboardOrderManager/RNCEKVOrderRelationship/RNCEKVOrderRelationship.mm @@ -35,6 +35,8 @@ - (void)update:(NSNumber*)lastPosition withPosition:(NSNumber*)position withObje -(void)clear { [_positions clear]; + self.entry = nil; + self.exit = nil; } - (int)getItemIndex:(UIView *)element { diff --git a/ios/Views/Base/FocusChange/RNCEKVViewFocusChangeBase.mm b/ios/Views/Base/FocusChange/RNCEKVViewFocusChangeBase.mm index e40290e..f634656 100644 --- a/ios/Views/Base/FocusChange/RNCEKVViewFocusChangeBase.mm +++ b/ios/Views/Base/FocusChange/RNCEKVViewFocusChangeBase.mm @@ -61,12 +61,11 @@ - (NSNumber *)resolveFocusChange:(UIFocusUpdateContext *)context { - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator { - _isFocused = [self resolveFocusChange:context]; + NSNumber *focusChange = [self resolveFocusChange:context]; - if ([self hasOnFocusChanged]) { - if (_isFocused != nil) { - [self onFocusChangeHandler:[_isFocused isEqual:@YES]]; - } + if (focusChange != nil) { + _isFocused = focusChange; + [self onFocusChangeHandler:[focusChange isEqual:@YES]]; } [super didUpdateFocusInContext:context withAnimationCoordinator:coordinator]; diff --git a/ios/Views/Base/FocusOrderGroup/RNCEKVViewOrderGroupBase.mm b/ios/Views/Base/FocusOrderGroup/RNCEKVViewOrderGroupBase.mm index 64d3be3..76a6997 100644 --- a/ios/Views/Base/FocusOrderGroup/RNCEKVViewOrderGroupBase.mm +++ b/ios/Views/Base/FocusOrderGroup/RNCEKVViewOrderGroupBase.mm @@ -8,7 +8,7 @@ #import #import "RNCEKVViewOrderGroupBase.h" #import "RNCEKVOrderLinking.h" -#import "UIViewController+RNCEKVExternalKeyboard.h" +#import "RNCEKVKeyboardFocusService.h" #import "UIView+React.h" #import "RNCEKVPropHelper.h" @@ -34,7 +34,21 @@ - (instancetype)initWithFrame:(CGRect)frame } - (BOOL)getIsViewFocused:(UIFocusUpdateContext *)context { - return context.nextFocusedView == [self getStoredView]; + UIView *next = context.nextFocusedView; + if (next == self) { + return YES; + } + if (next == nil || ![next isDescendantOfView:self]) { + return NO; + } + // The nearest order wrapper owns the focused view. Keep this wrapper's + // directional guides disabled when a nested wrapper owns focus. + for (UIView *view = next; view != nil && view != self; view = view.superview) { + if ([view isKindOfClass:[RNCEKVViewOrderGroupBase class]]) { + return NO; + } + } + return YES; } - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context @@ -48,7 +62,7 @@ - (void)focus { BOOL isAttached = self.superview != nil && controller != nil; if (isAttached) { - [controller rncekvFocusView:[self getStoredView]]; + [RNCEKVKeyboardFocusService focus:[self getStoredView] withFallback:controller]; } } diff --git a/ios/Views/Base/FocusRequest/RNCEKVViewFocusRequestBase.mm b/ios/Views/Base/FocusRequest/RNCEKVViewFocusRequestBase.mm index f3dcb71..98d5057 100644 --- a/ios/Views/Base/FocusRequest/RNCEKVViewFocusRequestBase.mm +++ b/ios/Views/Base/FocusRequest/RNCEKVViewFocusRequestBase.mm @@ -6,10 +6,11 @@ // #import -#import "UIViewController+RNCEKVExternalKeyboard.h" #import "UIView+React.h" #import "RNCEKVViewFocusRequestBase.h" +#import "RNCEKVKeyboardFocusService.h" +#import "UIViewController+RNCEKVExternalKeyboard.h" #ifdef RCT_NEW_ARCH_ENABLED #import "RNCEKVNativeProps.h" @@ -17,19 +18,34 @@ #endif @implementation RNCEKVViewFocusRequestBase { - BOOL _isAttachedToWindow; BOOL _autoFocusRequested; + BOOL _pendingFocusRequest; + BOOL _pendingScreenReaderFocus; + NSUInteger _autoFocusGeneration; + __weak UIViewController *_focusRoutedController; } - (void)cleanReferences { [super cleanReferences]; - _isAttachedToWindow = NO; + [self clearRoutedFocusTarget]; _autoFocusRequested = NO; + _pendingFocusRequest = NO; + _pendingScreenReaderFocus = NO; + _autoFocusGeneration++; +} + +// Clears this view's preferred-focus entry without removing a newer request +// from another view. +- (void)clearRoutedFocusTarget { + UIViewController *routedController = _focusRoutedController; + if (routedController != nil && routedController.rncekvCustomFocusView == self) { + routedController.rncekvCustomFocusView = nil; + } + _focusRoutedController = nil; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - _isAttachedToWindow = NO; _autoFocusRequested = NO; } @@ -38,12 +54,19 @@ - (instancetype)initWithFrame:(CGRect)frame { - (void)focus { UIViewController *controller = self.reactViewController; - if (controller != nil) { - [controller rncekvFocusView: self]; + if (controller == nil || self.window == nil) { + _pendingFocusRequest = YES; + return; } + _pendingFocusRequest = NO; + _focusRoutedController = [RNCEKVKeyboardFocusService focus:self withFallback:controller]; } - (void)screenReaderFocus { + if (self.window == nil) { + _pendingScreenReaderFocus = YES; + return; + } dispatch_async(dispatch_get_main_queue(), ^{ UIView *focusView = [self getFocusTargetView]; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, @@ -72,9 +95,23 @@ - (void)focusOnMount { if (self.autoFocus) { if(!_autoFocusRequested) { _autoFocusRequested = YES; + NSUInteger generation = _autoFocusGeneration; + __weak __typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{ - [self focus]; + __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil || strongSelf->_autoFocusGeneration != generation) { + return; + } + if (strongSelf.window == nil) { + // The view detached before autofocus ran. Let the next attachment + // try again. + strongSelf->_autoFocusRequested = NO; + return; + } + if (strongSelf.autoFocus) { + [strongSelf focus]; + } }); }); } @@ -86,14 +123,18 @@ - (void)didMoveToWindow { [super didMoveToWindow]; if (self.window) { - [self onAttached]; - } - - if (self.window && !_isAttachedToWindow) { - if (self.autoFocus) { + if (_pendingFocusRequest) { + _pendingFocusRequest = NO; [self focus]; } - _isAttachedToWindow = YES; + if (_pendingScreenReaderFocus) { + _pendingScreenReaderFocus = NO; + [self screenReaderFocus]; + } + [self onAttached]; + } else { + // A detached view must no longer be the controller's preferred target. + [self clearRoutedFocusTarget]; } } diff --git a/ios/Views/RNCEKVExternalKeyboardLockView/RNCEKVExternalKeyboardLockView.mm b/ios/Views/RNCEKVExternalKeyboardLockView/RNCEKVExternalKeyboardLockView.mm index c05268e..dc8084d 100644 --- a/ios/Views/RNCEKVExternalKeyboardLockView/RNCEKVExternalKeyboardLockView.mm +++ b/ios/Views/RNCEKVExternalKeyboardLockView/RNCEKVExternalKeyboardLockView.mm @@ -6,7 +6,7 @@ // #import -#import "UIViewController+RNCEKVExternalKeyboard.h" +#import "RNCEKVKeyboardFocusService.h" #import #import @@ -78,15 +78,21 @@ - (void)onAccessibilityFocusChanged:(NSNotification *)notification { } - (void)setForceLock:(BOOL)forceLock { + BOOL becameActive = forceLock && !_forceLock && !_lockDisabled; _forceLock = forceLock; - [self requestFocus]; - [self requestScreenReaderFocus]; + if (becameActive) { + [self requestFocus]; + [self requestScreenReaderFocus]; + } } - (void)setLockDisabled:(BOOL)lockDisabled { + BOOL becameActive = _forceLock && !lockDisabled && _lockDisabled; _lockDisabled = lockDisabled; - [self requestFocus]; - [self requestScreenReaderFocus]; + if (becameActive) { + [self requestFocus]; + [self requestScreenReaderFocus]; + } } - (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context { @@ -103,16 +109,16 @@ - (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context { } - (void)requestFocus { - if (!_forceLock && _lockDisabled) return; + if (!_forceLock || _lockDisabled) return; UIViewController *controller = self.reactViewController; if (controller != nil) { - [controller rncekvFocusView: self]; + [RNCEKVKeyboardFocusService focus:self withFallback:controller]; } } - (void)requestScreenReaderFocus { - if (!_forceLock && _lockDisabled) return; + if (!_forceLock || _lockDisabled) return; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self); } @@ -142,8 +148,14 @@ - (void)updateProps:(Props::Shared const &)props *std::static_pointer_cast(props); [super updateProps:props oldProps:oldProps]; - self.forceLock = newViewProps.forceLock; - self.lockDisabled = newViewProps.lockDisabled; + // Apply lockDisabled first so { forceLock: true, lockDisabled: true } never + // activates the trap between property updates. + if (_lockDisabled != newViewProps.lockDisabled) { + self.lockDisabled = newViewProps.lockDisabled; + } + if (_forceLock != newViewProps.forceLock) { + self.forceLock = newViewProps.forceLock; + } } Class ExternalKeyboardLockViewCls(void) @@ -156,6 +168,8 @@ - (void)updateProps:(Props::Shared const &)props - (void)didMoveToWindow { [super didMoveToWindow]; + // An active trap may request focus before it has a controller. Retry after + // attachment; the request guards exclude inactive and disabled traps. if (self.window) { [self requestFocus]; [self requestScreenReaderFocus]; diff --git a/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardView.mm b/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardView.mm index 27a1eb0..f5cf0b2 100644 --- a/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardView.mm +++ b/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardView.mm @@ -118,6 +118,9 @@ - (void)onBubbledContextMenuPressHandler { - (void)onFocusChangeHandler:(BOOL)isFocused { [super onFocusChangeHandler: isFocused]; + if (!self.hasOnFocusChanged) { + return; + } [RNCEKVFabricEventHelper onFocusChangeEventEmmiter:isFocused withEmitter:_eventEmitter]; } @@ -148,7 +151,7 @@ - (void)onBubbledContextMenuPressHandler { // - (void)onFocusChangeHandler:(BOOL)isFocused { [super onFocusChangeHandler: isFocused]; - if (self.onFocusChange) { + if (self.hasOnFocusChanged && self.onFocusChange) { self.onFocusChange(@{@"isFocused" : @(isFocused)}); } } diff --git a/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapper.mm b/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapper.mm index 491aecc..4f29dd2 100644 --- a/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapper.mm +++ b/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapper.mm @@ -6,6 +6,7 @@ #import "RNCEKVFocusEffectUtility.h" #import "RCTBaseTextInputView.h" #import "RNCEKVOrderLinking.h" +#import "RNCEKVKeyboardFocusService.h" #import "UIViewController+RNCEKVExternalKeyboard.h" #ifdef RCT_NEW_ARCH_ENABLED @@ -41,7 +42,11 @@ @interface RNCEKVTextInputFocusWrapper () static const NSInteger AUTO_FOCUS = 2; static const NSInteger AUTO_BLUR = 2; -@implementation RNCEKVTextInputFocusWrapper +@implementation RNCEKVTextInputFocusWrapper { + BOOL _pendingFocusRequest; + __weak UIViewController *_focusRoutedController; + __weak UIView *_focusRoutedTarget; +} - (instancetype)initWithFrame:(CGRect)frame { @@ -122,6 +127,9 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & #ifdef RCT_NEW_ARCH_ENABLED - (void)onFocusChangeHandler:(BOOL) isFocused { + if (!self.hasOnFocusChanged) { + return; + } if (_eventEmitter) { auto viewEventEmitter = std::static_pointer_cast(_eventEmitter); facebook::react::TextInputFocusWrapperEventEmitter::OnFocusChange data = { @@ -146,7 +154,7 @@ - (void)onMultiplyTextSubmitHandler: (RCTUITextView*) textView { - (void)onFocusChangeHandler:(BOOL) isFocused { - if(self.onFocusChange) { + if(self.hasOnFocusChanged && self.onFocusChange) { self.onFocusChange(@{ @"isFocused": @(isFocused) }); } } @@ -162,13 +170,45 @@ - (void)onMultiplyTextSubmitHandler: (RCTUITextView*) textView { - (void)focus { UIViewController *viewController = self.reactViewController; + if (viewController == nil || self.superview == nil || self.window == nil || + self.subviews.count == 0) { + _pendingFocusRequest = YES; + return; + } + _pendingFocusRequest = NO; [self updateFocus:viewController]; } - (void)updateFocus:(UIViewController *)controller { UIView *focusingView = self.subviews.count ? self.subviews[0] : nil; - if (self.superview != nil && controller != nil) { - [controller rncekvFocusView:focusingView]; + if (self.superview != nil && controller != nil && focusingView != nil) { + _focusRoutedController = [RNCEKVKeyboardFocusService focus:focusingView withFallback:controller]; + _focusRoutedTarget = focusingView; + } +} + +// Clears this child's preferred-focus entry without removing a newer request +// from another view. +- (void)clearRoutedFocusTarget { + UIViewController *routedController = _focusRoutedController; + UIView *routedTarget = _focusRoutedTarget; + if (routedController != nil && routedTarget != nil && + routedController.rncekvCustomFocusView == routedTarget) { + routedController.rncekvCustomFocusView = nil; + } + _focusRoutedController = nil; + _focusRoutedTarget = nil; +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + if (self.window) { + if (_pendingFocusRequest) { + _pendingFocusRequest = NO; + [self focus]; + } + } else { + [self clearRoutedFocusTarget]; } } @@ -180,16 +220,6 @@ - (UIView*)getStoredView { return _textField; } -- (NSNumber *)resolveFocusChange:(UIFocusUpdateContext *)context { - if([context.nextFocusedView isDescendantOfView:self]) { - return @YES; - } else if([context.previouslyFocusedView isDescendantOfView:self]) { - return @NO; - } - - return nil; -} - - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator { @@ -244,8 +274,10 @@ - (UIView*)getTextFieldComponent { - (void)cleanReferences{ [super cleanReferences]; + [self clearRoutedFocusTarget]; _textField = nil; _textView = nil; + _pendingFocusRequest = NO; } - (BOOL)getIsTextInputView: (UIView*)view { diff --git a/ios/features/Halo/delegate/RNCEKVHaloDelegate.mm b/ios/features/Halo/delegate/RNCEKVHaloDelegate.mm index 97c74f3..bc5765f 100644 --- a/ios/features/Halo/delegate/RNCEKVHaloDelegate.mm +++ b/ios/features/Halo/delegate/RNCEKVHaloDelegate.mm @@ -14,7 +14,7 @@ // — `haloCornerRadius`, `haloExpendX`, `haloExpendY`. The radius is an input, not // observed off the layer, so there is no stable-radius tracking and no re-arm loop. @implementation RNCEKVHaloDelegate { - UIView *_delegate; + __weak UIView *_delegate; UIFocusEffect *_currentEffect; BOOL _isDirty; CGRect _prevBounds;