Skip to content

Commit 1807c5b

Browse files
yogevbdguyca
authored andcommitted
Prevent creation of button react view with the same componentId (#5687)
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.
1 parent 57eb0db commit 1807c5b

7 files changed

+214
-160
lines changed

lib/ios/NSArray+utils.h

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import <Foundation/Foundation.h>
2+
3+
@interface NSArray (utils)
4+
5+
- (NSArray *)intersect:(NSArray *)array withPropertyName:(NSString *)propertyName;
6+
7+
- (NSArray *)difference:(NSArray *)array withPropertyName:(NSString *)propertyName;
8+
9+
- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block;
10+
11+
@end

lib/ios/NSArray+utils.m

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#import "NSArray+utils.h"
2+
3+
@implementation NSArray (utils)
4+
5+
- (NSArray *)intersect:(NSArray *)array withPropertyName:(NSString *)propertyName {
6+
NSMutableArray* intersection = [NSMutableArray new];
7+
for (NSObject* object in array) {
8+
NSArray* filteredArray = [self filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"%@ == %@", propertyName, object]];
9+
[intersection addObjectsFromArray:filteredArray];
10+
}
11+
12+
return [NSArray arrayWithArray:intersection];
13+
}
14+
15+
- (NSArray *)difference:(NSArray *)array withPropertyName:(NSString *)propertyName {
16+
NSMutableArray* diff = [NSMutableArray arrayWithArray:self];
17+
NSArray* intersection = [self intersect:array withPropertyName:propertyName];
18+
[diff removeObjectsInArray:intersection];
19+
20+
return [NSArray arrayWithArray:diff];
21+
}
22+
23+
- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block {
24+
NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
25+
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
26+
[result addObject:block(obj, idx)];
27+
}];
28+
return result;
29+
}
30+
31+
@end

lib/ios/RNNNavigationButtons.m

+151-137
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#import "UIImage+insets.h"
88
#import "UIViewController+LayoutProtocol.h"
99
#import "RNNFontAttributesCreator.h"
10+
#import "NSArray+utils.h"
1011

1112
@interface RNNNavigationButtons()
1213

@@ -19,167 +20,180 @@ @interface RNNNavigationButtons()
1920
@implementation RNNNavigationButtons
2021

2122
-(instancetype)initWithViewController:(UIViewController<RNNLayoutProtocol>*)viewController componentRegistry:(id)componentRegistry {
22-
self = [super init];
23-
24-
self.viewController = viewController;
25-
self.componentRegistry = componentRegistry;
26-
27-
return self;
23+
self = [super init];
24+
25+
self.viewController = viewController;
26+
self.componentRegistry = componentRegistry;
27+
28+
return self;
2829
}
2930

3031
- (void)applyLeftButtons:(NSArray *)leftButtons rightButtons:(NSArray *)rightButtons defaultLeftButtonStyle:(RNNButtonOptions *)defaultLeftButtonStyle defaultRightButtonStyle:(RNNButtonOptions *)defaultRightButtonStyle {
31-
_defaultLeftButtonStyle = defaultLeftButtonStyle;
32-
_defaultRightButtonStyle = defaultRightButtonStyle;
33-
if (leftButtons) {
34-
[self setButtons:leftButtons side:@"left" animated:NO defaultStyle:_defaultLeftButtonStyle insets:[self leftButtonInsets:_defaultLeftButtonStyle.iconInsets]];
35-
}
36-
37-
if (rightButtons) {
38-
[self setButtons:rightButtons side:@"right" animated:NO defaultStyle:_defaultRightButtonStyle insets:[self rightButtonInsets:_defaultRightButtonStyle.iconInsets]];
39-
}
32+
_defaultLeftButtonStyle = defaultLeftButtonStyle;
33+
_defaultRightButtonStyle = defaultRightButtonStyle;
34+
if (leftButtons) {
35+
[self setButtons:leftButtons side:@"left" animated:NO defaultStyle:_defaultLeftButtonStyle insets:[self leftButtonInsets:_defaultLeftButtonStyle.iconInsets]];
36+
}
37+
38+
if (rightButtons) {
39+
[self setButtons:rightButtons side:@"right" animated:NO defaultStyle:_defaultRightButtonStyle insets:[self rightButtonInsets:_defaultRightButtonStyle.iconInsets]];
40+
}
4041
}
4142

4243
-(void)setButtons:(NSArray*)buttons side:(NSString*)side animated:(BOOL)animated defaultStyle:(RNNButtonOptions *)defaultStyle insets:(UIEdgeInsets)insets {
43-
NSMutableArray *barButtonItems = [NSMutableArray new];
44-
NSArray* resolvedButtons = [self resolveButtons:buttons];
45-
for (NSDictionary *button in resolvedButtons) {
46-
RNNUIBarButtonItem* barButtonItem = [self buildButton:button defaultStyle:defaultStyle insets:insets];
47-
if(barButtonItem) {
48-
[barButtonItems addObject:barButtonItem];
49-
}
50-
UIColor* color = [self color:[RCTConvert UIColor:button[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
51-
if (color) {
52-
self.viewController.navigationController.navigationBar.tintColor = color;
53-
}
54-
}
55-
56-
if ([side isEqualToString:@"left"]) {
57-
[self.viewController.navigationItem setLeftBarButtonItems:barButtonItems animated:animated];
58-
}
59-
60-
if ([side isEqualToString:@"right"]) {
61-
[self.viewController.navigationItem setRightBarButtonItems:barButtonItems animated:animated];
62-
}
44+
NSMutableArray *barButtonItems = [NSMutableArray new];
45+
NSArray* resolvedButtons = [self resolveButtons:buttons];
46+
for (NSDictionary *button in resolvedButtons) {
47+
RNNUIBarButtonItem* barButtonItem = [self buildButton:button defaultStyle:defaultStyle insets:insets];
48+
if(barButtonItem) {
49+
[barButtonItems addObject:barButtonItem];
50+
}
51+
UIColor* color = [self color:[RCTConvert UIColor:button[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
52+
if (color) {
53+
self.viewController.navigationController.navigationBar.tintColor = color;
54+
}
55+
}
56+
57+
if ([side isEqualToString:@"left"]) {
58+
[self clearPreviousButtonViews:barButtonItems oldButtons:self.viewController.navigationItem.leftBarButtonItems];
59+
[self.viewController.navigationItem setLeftBarButtonItems:barButtonItems animated:animated];
60+
}
61+
62+
if ([side isEqualToString:@"right"]) {
63+
[self clearPreviousButtonViews:barButtonItems oldButtons:self.viewController.navigationItem.rightBarButtonItems];
64+
[self.viewController.navigationItem setRightBarButtonItems:barButtonItems animated:animated];
65+
}
66+
}
67+
68+
- (void)clearPreviousButtonViews:(NSArray<UIBarButtonItem *> *)newButtons oldButtons:(NSArray<UIBarButtonItem *> *)oldButtons {
69+
NSArray<UIBarButtonItem *>* removedButtons = [oldButtons difference:newButtons withPropertyName:@"customView"];
70+
71+
for (UIBarButtonItem* buttonItem in removedButtons) {
72+
RNNReactView* reactView = buttonItem.customView;
73+
if ([reactView isKindOfClass:[RNNReactView class]]) {
74+
[_componentRegistry removeChildComponent:reactView.componentId];
75+
}
76+
}
6377
}
6478

6579
- (NSArray *)resolveButtons:(id)buttons {
66-
if ([buttons isKindOfClass:[NSArray class]]) {
67-
return buttons;
68-
} else {
69-
return @[buttons];
70-
}
80+
if ([buttons isKindOfClass:[NSArray class]]) {
81+
return buttons;
82+
} else {
83+
return @[buttons];
84+
}
7185
}
7286

7387
-(RNNUIBarButtonItem*)buildButton: (NSDictionary*)dictionary defaultStyle:(RNNButtonOptions *)defaultStyle insets:(UIEdgeInsets)insets {
74-
NSString* buttonId = dictionary[@"id"];
75-
NSString* title = [self getValue:dictionary[@"text"] withDefault:[defaultStyle.text getWithDefaultValue:nil]];
76-
NSDictionary* component = dictionary[@"component"];
77-
NSString* systemItemName = dictionary[@"systemItem"];
78-
79-
UIColor* color = [self color:[RCTConvert UIColor:dictionary[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
80-
UIColor* disabledColor = [self color:[RCTConvert UIColor:dictionary[@"disabledColor"]] defaultColor:[defaultStyle.disabledColor getWithDefaultValue:nil]];
81-
82-
if (!buttonId) {
83-
@throw [NSException exceptionWithName:@"NSInvalidArgumentException" reason:[@"button id is not specified " stringByAppendingString:title] userInfo:nil];
84-
}
85-
86-
UIImage* defaultIcon = [defaultStyle.icon getWithDefaultValue:nil];
87-
UIImage* iconImage = [self getValue:dictionary[@"icon"] withDefault:defaultIcon];
88-
if (![iconImage isKindOfClass:[UIImage class]]) {
89-
iconImage = [RCTConvert UIImage:iconImage];
90-
}
91-
92-
if (iconImage) {
93-
iconImage = [iconImage imageWithInsets:insets];
94-
if (color) {
95-
iconImage = [iconImage withTintColor:color];
96-
}
97-
}
98-
99-
100-
RNNUIBarButtonItem *barButtonItem;
101-
if (component) {
102-
RNNComponentOptions* componentOptions = [RNNComponentOptions new];
103-
componentOptions.componentId = [[Text alloc] initWithValue:component[@"componentId"]];
104-
componentOptions.name = [[Text alloc] initWithValue:component[@"name"]];
105-
106-
RNNReactView *view = [_componentRegistry createComponentIfNotExists:componentOptions parentComponentId:self.viewController.layoutInfo.componentId reactViewReadyBlock:nil];
107-
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withCustomView:view componentRegistry:_componentRegistry];
108-
} else if (iconImage) {
109-
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withIcon:iconImage];
110-
} else if (title) {
111-
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withTitle:title];
112-
113-
NSMutableDictionary *buttonTextAttributes = [RCTHelpers textAttributesFromDictionary:dictionary withPrefix:@"button"];
114-
if (buttonTextAttributes.allKeys.count > 0) {
115-
[barButtonItem setTitleTextAttributes:buttonTextAttributes forState:UIControlStateNormal];
116-
}
117-
} else if (systemItemName) {
118-
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withSystemItem:systemItemName];
119-
} else {
120-
return nil;
121-
}
122-
123-
barButtonItem.target = self.viewController;
124-
barButtonItem.action = @selector(onButtonPress:);
125-
126-
NSNumber *enabled = [self getValue:dictionary[@"enabled"] withDefault:defaultStyle.enabled.getValue];
127-
BOOL enabledBool = enabled ? [enabled boolValue] : YES;
128-
[barButtonItem setEnabled:enabledBool];
129-
130-
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]];
131-
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]];
132-
133-
if (!enabledBool && disabledColor) {
134-
color = disabledColor;
135-
[disabledTextAttributes setObject:disabledColor forKey:NSForegroundColorAttributeName];
136-
}
137-
138-
if (color) {
139-
[textAttributes setObject:color forKey:NSForegroundColorAttributeName];
140-
[barButtonItem setImage:[[iconImage withTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
141-
barButtonItem.tintColor = color;
142-
}
143-
144-
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
145-
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateHighlighted];
146-
[barButtonItem setTitleTextAttributes:disabledTextAttributes forState:UIControlStateDisabled];
147-
148-
NSString *testID = dictionary[@"testID"];
149-
if (testID)
150-
{
151-
barButtonItem.accessibilityIdentifier = testID;
152-
}
153-
154-
return barButtonItem;
88+
NSString* buttonId = dictionary[@"id"];
89+
NSString* title = [self getValue:dictionary[@"text"] withDefault:[defaultStyle.text getWithDefaultValue:nil]];
90+
NSDictionary* component = dictionary[@"component"];
91+
NSString* systemItemName = dictionary[@"systemItem"];
92+
93+
UIColor* color = [self color:[RCTConvert UIColor:dictionary[@"color"]] defaultColor:[defaultStyle.color getWithDefaultValue:nil]];
94+
UIColor* disabledColor = [self color:[RCTConvert UIColor:dictionary[@"disabledColor"]] defaultColor:[defaultStyle.disabledColor getWithDefaultValue:nil]];
95+
96+
if (!buttonId) {
97+
@throw [NSException exceptionWithName:@"NSInvalidArgumentException" reason:[@"button id is not specified " stringByAppendingString:title] userInfo:nil];
98+
}
99+
100+
UIImage* defaultIcon = [defaultStyle.icon getWithDefaultValue:nil];
101+
UIImage* iconImage = [self getValue:dictionary[@"icon"] withDefault:defaultIcon];
102+
if (![iconImage isKindOfClass:[UIImage class]]) {
103+
iconImage = [RCTConvert UIImage:iconImage];
104+
}
105+
106+
if (iconImage) {
107+
iconImage = [iconImage imageWithInsets:insets];
108+
if (color) {
109+
iconImage = [iconImage withTintColor:color];
110+
}
111+
}
112+
113+
114+
RNNUIBarButtonItem *barButtonItem;
115+
if (component) {
116+
RNNComponentOptions* componentOptions = [RNNComponentOptions new];
117+
componentOptions.componentId = [[Text alloc] initWithValue:component[@"componentId"]];
118+
componentOptions.name = [[Text alloc] initWithValue:component[@"name"]];
119+
120+
RNNReactView *view = [_componentRegistry createComponentIfNotExists:componentOptions parentComponentId:self.viewController.layoutInfo.componentId reactViewReadyBlock:nil];
121+
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withCustomView:view];
122+
} else if (iconImage) {
123+
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withIcon:iconImage];
124+
} else if (title) {
125+
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withTitle:title];
126+
127+
NSMutableDictionary *buttonTextAttributes = [RCTHelpers textAttributesFromDictionary:dictionary withPrefix:@"button"];
128+
if (buttonTextAttributes.allKeys.count > 0) {
129+
[barButtonItem setTitleTextAttributes:buttonTextAttributes forState:UIControlStateNormal];
130+
}
131+
} else if (systemItemName) {
132+
barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withSystemItem:systemItemName];
133+
} else {
134+
return nil;
135+
}
136+
137+
barButtonItem.target = self.viewController;
138+
barButtonItem.action = @selector(onButtonPress:);
139+
140+
NSNumber *enabled = [self getValue:dictionary[@"enabled"] withDefault:defaultStyle.enabled.getValue];
141+
BOOL enabledBool = enabled ? [enabled boolValue] : YES;
142+
[barButtonItem setEnabled:enabledBool];
143+
144+
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]];
145+
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]];
146+
147+
if (!enabledBool && disabledColor) {
148+
color = disabledColor;
149+
[disabledTextAttributes setObject:disabledColor forKey:NSForegroundColorAttributeName];
150+
}
151+
152+
if (color) {
153+
[textAttributes setObject:color forKey:NSForegroundColorAttributeName];
154+
[barButtonItem setImage:[[iconImage withTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
155+
barButtonItem.tintColor = color;
156+
}
157+
158+
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
159+
[barButtonItem setTitleTextAttributes:textAttributes forState:UIControlStateHighlighted];
160+
[barButtonItem setTitleTextAttributes:disabledTextAttributes forState:UIControlStateDisabled];
161+
162+
NSString *testID = dictionary[@"testID"];
163+
if (testID)
164+
{
165+
barButtonItem.accessibilityIdentifier = testID;
166+
}
167+
168+
return barButtonItem;
155169
}
156170

157171
- (UIColor *)color:(UIColor *)color defaultColor:(UIColor *)defaultColor {
158-
if (color) {
159-
return color;
160-
} else if (defaultColor) {
161-
return defaultColor;
162-
}
163-
164-
return nil;
172+
if (color) {
173+
return color;
174+
} else if (defaultColor) {
175+
return defaultColor;
176+
}
177+
178+
return nil;
165179
}
166180

167181
- (id)getValue:(id)value withDefault:(id)defaultValue {
168-
return value ? value : defaultValue;
182+
return value ? value : defaultValue;
169183
}
170184

171185
- (UIEdgeInsets)leftButtonInsets:(RNNInsetsOptions *)defaultInsets {
172-
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
173-
[defaultInsets.left getWithDefaultValue:0],
174-
[defaultInsets.bottom getWithDefaultValue:0],
175-
[defaultInsets.right getWithDefaultValue:15]);
186+
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
187+
[defaultInsets.left getWithDefaultValue:0],
188+
[defaultInsets.bottom getWithDefaultValue:0],
189+
[defaultInsets.right getWithDefaultValue:15]);
176190
}
177191

178192
- (UIEdgeInsets)rightButtonInsets:(RNNInsetsOptions *)defaultInsets {
179-
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
180-
[defaultInsets.left getWithDefaultValue:15],
181-
[defaultInsets.bottom getWithDefaultValue:0],
182-
[defaultInsets.right getWithDefaultValue:0]);
193+
return UIEdgeInsetsMake([defaultInsets.top getWithDefaultValue:0],
194+
[defaultInsets.left getWithDefaultValue:15],
195+
[defaultInsets.bottom getWithDefaultValue:0],
196+
[defaultInsets.right getWithDefaultValue:0]);
183197
}
184198

185199
@end

0 commit comments

Comments
 (0)