Skip to content

Commit

Permalink
Prevent creation of button react view with the same componentId (#5687)
Browse files Browse the repository at this point in the history
When updating buttons with mergeOptions, existing custom buttons are unmounted and then recreated. This commit adds an optimisation which prevents the react components from being recreated.
To avoid unnecessary recreation, add an `id` to the component. If an existing react view is found for that id, it will be reused.
  • Loading branch information
yogevbd authored and guyca committed Nov 27, 2019
1 parent 57eb0db commit 1807c5b
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 160 deletions.
11 changes: 11 additions & 0 deletions lib/ios/NSArray+utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>

@interface NSArray (utils)

- (NSArray *)intersect:(NSArray *)array withPropertyName:(NSString *)propertyName;

- (NSArray *)difference:(NSArray *)array withPropertyName:(NSString *)propertyName;

- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block;

@end
31 changes: 31 additions & 0 deletions lib/ios/NSArray+utils.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#import "NSArray+utils.h"

@implementation NSArray (utils)

- (NSArray *)intersect:(NSArray *)array withPropertyName:(NSString *)propertyName {
NSMutableArray* intersection = [NSMutableArray new];
for (NSObject* object in array) {
NSArray* filteredArray = [self filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"%@ == %@", propertyName, object]];
[intersection addObjectsFromArray:filteredArray];
}

return [NSArray arrayWithArray:intersection];
}

- (NSArray *)difference:(NSArray *)array withPropertyName:(NSString *)propertyName {
NSMutableArray* diff = [NSMutableArray arrayWithArray:self];
NSArray* intersection = [self intersect:array withPropertyName:propertyName];
[diff removeObjectsInArray:intersection];

return [NSArray arrayWithArray:diff];
}

- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[result addObject:block(obj, idx)];
}];
return result;
}

@end
288 changes: 151 additions & 137 deletions lib/ios/RNNNavigationButtons.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#import "UIImage+insets.h"
#import "UIViewController+LayoutProtocol.h"
#import "RNNFontAttributesCreator.h"
#import "NSArray+utils.h"

@interface RNNNavigationButtons()

Expand All @@ -19,167 +20,180 @@ @interface RNNNavigationButtons()
@implementation RNNNavigationButtons

-(instancetype)initWithViewController:(UIViewController<RNNLayoutProtocol>*)viewController componentRegistry:(id)componentRegistry {
self = [super init];
self.viewController = viewController;
self.componentRegistry = componentRegistry;
return self;
self = [super init];
self.viewController = viewController;
self.componentRegistry = componentRegistry;
return self;
}

- (void)applyLeftButtons:(NSArray *)leftButtons rightButtons:(NSArray *)rightButtons defaultLeftButtonStyle:(RNNButtonOptions *)defaultLeftButtonStyle defaultRightButtonStyle:(RNNButtonOptions *)defaultRightButtonStyle {
_defaultLeftButtonStyle = defaultLeftButtonStyle;
_defaultRightButtonStyle = defaultRightButtonStyle;
if (leftButtons) {
[self setButtons:leftButtons side:@"left" animated:NO defaultStyle:_defaultLeftButtonStyle insets:[self leftButtonInsets:_defaultLeftButtonStyle.iconInsets]];
}
if (rightButtons) {
[self setButtons:rightButtons side:@"right" animated:NO defaultStyle:_defaultRightButtonStyle insets:[self rightButtonInsets:_defaultRightButtonStyle.iconInsets]];
}
_defaultLeftButtonStyle = defaultLeftButtonStyle;
_defaultRightButtonStyle = defaultRightButtonStyle;
if (leftButtons) {
[self setButtons:leftButtons side:@"left" animated:NO defaultStyle:_defaultLeftButtonStyle insets:[self leftButtonInsets:_defaultLeftButtonStyle.iconInsets]];
}
if (rightButtons) {
[self setButtons:rightButtons side:@"right" animated:NO defaultStyle:_defaultRightButtonStyle insets:[self rightButtonInsets:_defaultRightButtonStyle.iconInsets]];
}
}

-(void)setButtons:(NSArray*)buttons side:(NSString*)side animated:(BOOL)animated defaultStyle:(RNNButtonOptions *)defaultStyle insets:(UIEdgeInsets)insets {
NSMutableArray *barButtonItems = [NSMutableArray new];
NSArray* resolvedButtons = [self resolveButtons:buttons];
for (NSDictionary *button in resolvedButtons) {
RNNUIBarButtonItem* barButtonItem = [self buildButton:button defaultStyle:defaultStyle insets:insets];
if(barButtonItem) {
[barButtonItems addObject:barButtonItem];
}
UIColor* color = [self color:[RCTConvert UIColor:button[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
if (color) {
self.viewController.navigationController.navigationBar.tintColor = color;
}
}

if ([side isEqualToString:@"left"]) {
[self.viewController.navigationItem setLeftBarButtonItems:barButtonItems animated:animated];
}

if ([side isEqualToString:@"right"]) {
[self.viewController.navigationItem setRightBarButtonItems:barButtonItems animated:animated];
}
NSMutableArray *barButtonItems = [NSMutableArray new];
NSArray* resolvedButtons = [self resolveButtons:buttons];
for (NSDictionary *button in resolvedButtons) {
RNNUIBarButtonItem* barButtonItem = [self buildButton:button defaultStyle:defaultStyle insets:insets];
if(barButtonItem) {
[barButtonItems addObject:barButtonItem];
}
UIColor* color = [self color:[RCTConvert UIColor:button[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
if (color) {
self.viewController.navigationController.navigationBar.tintColor = color;
}
}

if ([side isEqualToString:@"left"]) {
[self clearPreviousButtonViews:barButtonItems oldButtons:self.viewController.navigationItem.leftBarButtonItems];
[self.viewController.navigationItem setLeftBarButtonItems:barButtonItems animated:animated];
}

if ([side isEqualToString:@"right"]) {
[self clearPreviousButtonViews:barButtonItems oldButtons:self.viewController.navigationItem.rightBarButtonItems];
[self.viewController.navigationItem setRightBarButtonItems:barButtonItems animated:animated];
}
}

- (void)clearPreviousButtonViews:(NSArray<UIBarButtonItem *> *)newButtons oldButtons:(NSArray<UIBarButtonItem *> *)oldButtons {
NSArray<UIBarButtonItem *>* removedButtons = [oldButtons difference:newButtons withPropertyName:@"customView"];

for (UIBarButtonItem* buttonItem in removedButtons) {
RNNReactView* reactView = buttonItem.customView;
if ([reactView isKindOfClass:[RNNReactView class]]) {
[_componentRegistry removeChildComponent:reactView.componentId];
}
}
}

- (NSArray *)resolveButtons:(id)buttons {
if ([buttons isKindOfClass:[NSArray class]]) {
return buttons;
} else {
return @[buttons];
}
if ([buttons isKindOfClass:[NSArray class]]) {
return buttons;
} else {
return @[buttons];
}
}

-(RNNUIBarButtonItem*)buildButton: (NSDictionary*)dictionary defaultStyle:(RNNButtonOptions *)defaultStyle insets:(UIEdgeInsets)insets {
NSString* buttonId = dictionary[@"id"];
NSString* title = [self getValue:dictionary[@"text"] withDefault:[defaultStyle.text getWithDefaultValue:nil]];
NSDictionary* component = dictionary[@"component"];
NSString* systemItemName = dictionary[@"systemItem"];
UIColor* color = [self color:[RCTConvert UIColor:dictionary[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
UIColor* disabledColor = [self color:[RCTConvert UIColor:dictionary[@"disabledColor"]] defaultColor:[defaultStyle.disabledColor getWithDefaultValue:nil]];
if (!buttonId) {
@throw [NSException exceptionWithName:@"NSInvalidArgumentException" reason:[@"button id is not specified " stringByAppendingString:title] userInfo:nil];
}
UIImage* defaultIcon = [defaultStyle.icon getWithDefaultValue:nil];
UIImage* iconImage = [self getValue:dictionary[@"icon"] withDefault:defaultIcon];
if (![iconImage isKindOfClass:[UIImage class]]) {
iconImage = [RCTConvert UIImage:iconImage];
}
if (iconImage) {
iconImage = [iconImage imageWithInsets:insets];
if (color) {
iconImage = [iconImage withTintColor:color];
}
}
RNNUIBarButtonItem *barButtonItem;
if (component) {
RNNComponentOptions* componentOptions = [RNNComponentOptions new];
componentOptions.componentId = [[Text alloc] initWithValue:component[@"componentId"]];
componentOptions.name = [[Text alloc] initWithValue:component[@"name"]];
RNNReactView *view = [_componentRegistry createComponentIfNotExists:componentOptions parentComponentId:self.viewController.layoutInfo.componentId reactViewReadyBlock:nil];
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withCustomView:view componentRegistry:_componentRegistry];
} else if (iconImage) {
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withIcon:iconImage];
} else if (title) {
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withTitle:title];
NSMutableDictionary *buttonTextAttributes = [RCTHelpers textAttributesFromDictionary:dictionary withPrefix:@"button"];
if (buttonTextAttributes.allKeys.count > 0) {
[barButtonItem setTitleTextAttributes:buttonTextAttributes forState:UIControlStateNormal];
}
} else if (systemItemName) {
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withSystemItem:systemItemName];
} else {
return nil;
}
barButtonItem.target = self.viewController;
barButtonItem.action = @selector(onButtonPress:);
NSNumber *enabled = [self getValue:dictionary[@"enabled"] withDefault:defaultStyle.enabled.getValue];
BOOL enabledBool = enabled ? [enabled boolValue] : YES;
[barButtonItem setEnabled:enabledBool];
NSMutableDictionary* textAttributes = [NSMutableDictionary dictionaryWithDictionary:[RNNFontAttributesCreator createWithFontFamily:dictionary[@"fontFamily"] ?: [defaultStyle.fontFamily getWithDefaultValue:nil] fontSize:dictionary[@"fontSize"] defaultFontSize:[defaultStyle.fontSize getWithDefaultValue:@(17)] fontWeight:dictionary[@"fontWeight"] color:nil defaultColor:nil]];
NSMutableDictionary* disabledTextAttributes = [NSMutableDictionary dictionaryWithDictionary:[RNNFontAttributesCreator createWithFontFamily:dictionary[@"fontFamily"] ?: [defaultStyle.fontFamily getWithDefaultValue:nil] fontSize:dictionary[@"fontSize"] defaultFontSize:[defaultStyle.fontSize getWithDefaultValue:@(17)] fontWeight:dictionary[@"fontWeight"] color:nil defaultColor:nil]];
if (!enabledBool && disabledColor) {
color = disabledColor;
[disabledTextAttributes setObject:disabledColor forKey:NSForegroundColorAttributeName];
}
if (color) {
[textAttributes setObject:color forKey:NSForegroundColorAttributeName];
[barButtonItem setImage:[[iconImage withTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
barButtonItem.tintColor = color;
}

[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateHighlighted];
[barButtonItem setTitleTextAttributes:disabledTextAttributes forState:UIControlStateDisabled];
NSString *testID = dictionary[@"testID"];
if (testID)
{
barButtonItem.accessibilityIdentifier = testID;
}
return barButtonItem;
NSString* buttonId = dictionary[@"id"];
NSString* title = [self getValue:dictionary[@"text"] withDefault:[defaultStyle.text getWithDefaultValue:nil]];
NSDictionary* component = dictionary[@"component"];
NSString* systemItemName = dictionary[@"systemItem"];
UIColor* color = [self color:[RCTConvert UIColor:dictionary[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
UIColor* disabledColor = [self color:[RCTConvert UIColor:dictionary[@"disabledColor"]] defaultColor:[defaultStyle.disabledColor getWithDefaultValue:nil]];
if (!buttonId) {
@throw [NSException exceptionWithName:@"NSInvalidArgumentException" reason:[@"button id is not specified " stringByAppendingString:title] userInfo:nil];
}
UIImage* defaultIcon = [defaultStyle.icon getWithDefaultValue:nil];
UIImage* iconImage = [self getValue:dictionary[@"icon"] withDefault:defaultIcon];
if (![iconImage isKindOfClass:[UIImage class]]) {
iconImage = [RCTConvert UIImage:iconImage];
}
if (iconImage) {
iconImage = [iconImage imageWithInsets:insets];
if (color) {
iconImage = [iconImage withTintColor:color];
}
}
RNNUIBarButtonItem *barButtonItem;
if (component) {
RNNComponentOptions* componentOptions = [RNNComponentOptions new];
componentOptions.componentId = [[Text alloc] initWithValue:component[@"componentId"]];
componentOptions.name = [[Text alloc] initWithValue:component[@"name"]];
RNNReactView *view = [_componentRegistry createComponentIfNotExists:componentOptions parentComponentId:self.viewController.layoutInfo.componentId reactViewReadyBlock:nil];
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withCustomView:view];
} else if (iconImage) {
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withIcon:iconImage];
} else if (title) {
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withTitle:title];
NSMutableDictionary *buttonTextAttributes = [RCTHelpers textAttributesFromDictionary:dictionary withPrefix:@"button"];
if (buttonTextAttributes.allKeys.count > 0) {
[barButtonItem setTitleTextAttributes:buttonTextAttributes forState:UIControlStateNormal];
}
} else if (systemItemName) {
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withSystemItem:systemItemName];
} else {
return nil;
}
barButtonItem.target = self.viewController;
barButtonItem.action = @selector(onButtonPress:);
NSNumber *enabled = [self getValue:dictionary[@"enabled"] withDefault:defaultStyle.enabled.getValue];
BOOL enabledBool = enabled ? [enabled boolValue] : YES;
[barButtonItem setEnabled:enabledBool];
NSMutableDictionary* textAttributes = [NSMutableDictionary dictionaryWithDictionary:[RNNFontAttributesCreator createWithFontFamily:dictionary[@"fontFamily"] ?: [defaultStyle.fontFamily getWithDefaultValue:nil] fontSize:dictionary[@"fontSize"] defaultFontSize:[defaultStyle.fontSize getWithDefaultValue:@(17)] fontWeight:dictionary[@"fontWeight"] color:nil defaultColor:nil]];
NSMutableDictionary* disabledTextAttributes = [NSMutableDictionary dictionaryWithDictionary:[RNNFontAttributesCreator createWithFontFamily:dictionary[@"fontFamily"] ?: [defaultStyle.fontFamily getWithDefaultValue:nil] fontSize:dictionary[@"fontSize"] defaultFontSize:[defaultStyle.fontSize getWithDefaultValue:@(17)] fontWeight:dictionary[@"fontWeight"] color:nil defaultColor:nil]];
if (!enabledBool && disabledColor) {
color = disabledColor;
[disabledTextAttributes setObject:disabledColor forKey:NSForegroundColorAttributeName];
}
if (color) {
[textAttributes setObject:color forKey:NSForegroundColorAttributeName];
[barButtonItem setImage:[[iconImage withTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
barButtonItem.tintColor = color;
}
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateHighlighted];
[barButtonItem setTitleTextAttributes:disabledTextAttributes forState:UIControlStateDisabled];
NSString *testID = dictionary[@"testID"];
if (testID)
{
barButtonItem.accessibilityIdentifier = testID;
}
return barButtonItem;
}

- (UIColor *)color:(UIColor *)color defaultColor:(UIColor *)defaultColor {
if (color) {
return color;
} else if (defaultColor) {
return defaultColor;
}
return nil;
if (color) {
return color;
} else if (defaultColor) {
return defaultColor;
}
return nil;
}

- (id)getValue:(id)value withDefault:(id)defaultValue {
return value ? value : defaultValue;
return value ? value : defaultValue;
}

- (UIEdgeInsets)leftButtonInsets:(RNNInsetsOptions *)defaultInsets {
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
[defaultInsets.left getWithDefaultValue:0],
[defaultInsets.bottom getWithDefaultValue:0],
[defaultInsets.right getWithDefaultValue:15]);
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
[defaultInsets.left getWithDefaultValue:0],
[defaultInsets.bottom getWithDefaultValue:0],
[defaultInsets.right getWithDefaultValue:15]);
}

- (UIEdgeInsets)rightButtonInsets:(RNNInsetsOptions *)defaultInsets {
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
[defaultInsets.left getWithDefaultValue:15],
[defaultInsets.bottom getWithDefaultValue:0],
[defaultInsets.right getWithDefaultValue:0]);
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
[defaultInsets.left getWithDefaultValue:15],
[defaultInsets.bottom getWithDefaultValue:0],
[defaultInsets.right getWithDefaultValue:0]);
}

@end
Loading

0 comments on commit 1807c5b

Please sign in to comment.