Skip to content
22 changes: 14 additions & 8 deletions ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
#import "RNCEKVFocusProtocol.h"

@implementation RNCEKVFocusDelegate{
UIView<RNCEKVFocusProtocol>* _delegate;
__weak UIView<RNCEKVFocusProtocol>* _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<RNCEKVFocusProtocol> *_Nonnull)delegate{
Expand All @@ -28,6 +31,7 @@ - (instancetype _Nonnull )initWithView:(UIView<RNCEKVFocusProtocol> *_Nonnull)de

- (void)reset {
_focusedTarget = nil;
_isTrackingFocus = NO;
}

// Whether `view` is the focus target THIS wrapper owns. A non-wrapper owns only
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

@implementation RNCEKVFocusLinkDelegate {
BOOL _isFocused;
UIView<RNCEKVFocusOrderProtocol> *_delegate;
__weak UIView<RNCEKVFocusOrderProtocol> *_delegate;
NSMutableDictionary<NSNumber *, UIFocusGuide *> *_sides;
NSMutableDictionary<NSNumber *, RNCEKVOrderSubscriber *> *_subscribers;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
#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;
static NSNumber *const FOCUS_HANDLED = @0;

@implementation RNCEKVFocusSequenceDelegate {
BOOL _isLinked;
UIView<RNCEKVFocusOrderProtocol> *_delegate;
__weak UIView<RNCEKVFocusOrderProtocol> *_delegate;
}

- (instancetype)initWithView:(UIView<RNCEKVFocusOrderProtocol> *)delegate {
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


@implementation RNCEKVGroupIdentifierDelegate {
UIView<RNCEKVGroupIdentifierProtocol>* _delegate;
__weak UIView<RNCEKVGroupIdentifierProtocol>* _delegate;
NSString* _tagId;
}

Expand Down
2 changes: 1 addition & 1 deletion ios/Extensions/UIViewController+RNCEKVExternalKeyboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#import <UIKit/UIKit.h>

@interface UIViewController (RNCEKVExternalKeyboard)
@property (nonatomic, strong) UIView *rncekvCustomFocusView;
@property (nonatomic, weak) UIView *rncekvCustomFocusView;
- (void)rncekvFocusView:(UIView *)view;
@end

Expand Down
31 changes: 25 additions & 6 deletions ios/Extensions/UIViewController+RNCEKVExternalKeyboard.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -49,13 +62,19 @@ - (void)rncekvFocusView:(UIView *)view {
- (NSArray<id<UIFocusEnvironment>> *)keyboardedPreferredFocusEnvironments {
NSArray<id<UIFocusEnvironment>> *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;
}

Expand Down
6 changes: 6 additions & 0 deletions ios/Services/RNCEKVKeyboardFocusService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
15 changes: 10 additions & 5 deletions ios/Services/RNCEKVKeyboardFocusService.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
SynurDevelopers marked this conversation as resolved.
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 4 additions & 5 deletions ios/Views/Base/FocusChange/RNCEKVViewFocusChangeBase.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
20 changes: 17 additions & 3 deletions ios/Views/Base/FocusOrderGroup/RNCEKVViewOrderGroupBase.mm
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#import <Foundation/Foundation.h>
#import "RNCEKVViewOrderGroupBase.h"
#import "RNCEKVOrderLinking.h"
#import "UIViewController+RNCEKVExternalKeyboard.h"
#import "RNCEKVKeyboardFocusService.h"
#import "UIView+React.h"
#import "RNCEKVPropHelper.h"

Expand All @@ -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
Expand All @@ -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];
}
}

Expand Down
Loading