| ... | @@ -226,7 +226,45 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -226,7 +226,45 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 226 | | 226 | |
| 227 | // appearance does not apply to the tvOS so we need to use lagacy customization | 227 | // appearance does not apply to the tvOS so we need to use lagacy customization |
| 228 | #if TARGET_OS_TV | 228 | #if TARGET_OS_TV |
| 229 | @@ -637,10 +675,458 @@ + (void)updateViewController:(UIViewController *)vc | 229 | @@ -550,31 +588,25 @@ + (void)updateViewController:(UIViewController *)vc |
| | 230 | navitem.titleView = nil; |
| | 231 | - navitem.leftBarButtonItems = nil; |
| | 232 | - navitem.rightBarButtonItems = nil; |
| | 233 | - |
| | 234 | + |
| | 235 | #if !TARGET_OS_TV |
| | 236 | // We want to set navitem.searchController to nil only if we are sure |
| | 237 | // that we are removing the search bar from the header. |
| | 238 | bool searchBarPresent = false; |
| | 239 | #endif /* !TARGET_OS_TV */ |
| | 240 | - |
| | 241 | + |
| | 242 | + NSMutableArray<UIBarButtonItem *> *subviewLeftItems = [NSMutableArray array]; |
| | 243 | + NSMutableArray<UIBarButtonItem *> *subviewRightItems = [NSMutableArray array]; |
| | 244 | for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) { |
| | 245 | // This code should be kept in sync on Fabric with analogous switch statement in |
| | 246 | // `- [RNSScreenStackHeaderConfig replaceNavigationBarViewsWithSnapshotOfSubview:]` method. |
| | 247 | switch (subview.type) { |
| | 248 | case RNSScreenStackHeaderSubviewTypeLeft: { |
| | 249 | - NSArray<UIBarButtonItem *> *currentItems = navitem.leftBarButtonItems ?: @[]; |
| | 250 | - NSMutableArray<UIBarButtonItem *> *mutableItems = [currentItems mutableCopy]; |
| | 251 | - [mutableItems addObject:[subview getUIBarButtonItem]]; |
| | 252 | - navitem.leftBarButtonItems = mutableItems; |
| | 253 | + [subviewLeftItems addObject:[subview getUIBarButtonItem]]; |
| | 254 | break; |
| | 255 | } |
| | 256 | case RNSScreenStackHeaderSubviewTypeRight: { |
| | 257 | - NSArray<UIBarButtonItem *> *currentItems = navitem.rightBarButtonItems ?: @[]; |
| | 258 | - NSMutableArray<UIBarButtonItem *> *mutableItems = [currentItems mutableCopy]; |
| | 259 | - [mutableItems addObject:[subview getUIBarButtonItem]]; |
| | 260 | - navitem.rightBarButtonItems = mutableItems; |
| | 261 | + [subviewRightItems addObject:[subview getUIBarButtonItem]]; |
| | 262 | break; |
| | 263 | } |
| | 264 | case RNSScreenStackHeaderSubviewTypeCenter: |
| | 265 | case RNSScreenStackHeaderSubviewTypeTitle: { |
| | 266 | navitem.titleView = subview; |
| | 267 | @@ -637,10 +669,467 @@ + (void)updateViewController:(UIViewController *)vc |
| 230 | // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). | 268 | // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). |
| 231 | // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) | 269 | // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) |
| 232 | navitem.title = config.title; | 270 | navitem.title = config.title; |
| ... | @@ -237,29 +275,43 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -237,29 +275,43 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 237 | + NSArray *headerLeftConfigs = config.headerLeftBarButtonItems ?: @[]; | 275 | + NSArray *headerLeftConfigs = config.headerLeftBarButtonItems ?: @[]; |
| 238 | + NSArray *headerRightConfigs = config.headerRightBarButtonItems ?: @[]; | 276 | + NSArray *headerRightConfigs = config.headerRightBarButtonItems ?: @[]; |
| 239 | + NSArray *headerCenterConfigs = config.headerCenterBarButtonItems ?: @[]; | 277 | + NSArray *headerCenterConfigs = config.headerCenterBarButtonItems ?: @[]; |
| 240 | + NSArray<UIBarButtonItem *> *subviewLeftItems = navitem.leftBarButtonItems ?: @[]; | 278 | + // Retain the config in the key so its address cannot be reused while cached. |
| 241 | + NSArray<UIBarButtonItem *> *subviewRightItems = navitem.rightBarButtonItems ?: @[]; | 279 | + // Items capture this config's event emitter in their press handlers, so a |
| 242 | + // The key includes the config instance's identity: cached items capture | 280 | + // remounted header-config view with value-equal configs must still rebuild |
| 243 | + // this config's event emitter in their press handlers, so a remounted | 281 | + // instead of dispatching presses into the old config's emitter. |
| 244 | + // header-config view with value-equal configs must still rebuild — reusing | 282 | + // getUIBarButtonItem is stable for the lifetime of its React header |
| 245 | + // the old items would dispatch presses into the dead config's emitter. | 283 | + // subview. Include those native identities in the key so React-backed |
| 246 | + NSArray *headerItemsKey = | 284 | + // items can use the same cache without reusing a group after its view was |
| 247 | + @[ @((uintptr_t)config), headerLeftConfigs, headerRightConfigs, headerCenterConfigs ]; | 285 | + // replaced. |
| | 286 | + NSArray *headerItemsKey = @[ |
| | 287 | + config, |
| | 288 | + headerLeftConfigs, |
| | 289 | + headerRightConfigs, |
| | 290 | + headerCenterConfigs, |
| | 291 | + subviewLeftItems, |
| | 292 | + subviewRightItems, |
| | 293 | + ]; |
| 248 | + // Rebuilding bar button items creates brand-new native buttons (glass | 294 | + // Rebuilding bar button items creates brand-new native buttons (glass |
| 249 | + // UIButton custom views on iOS 26). Replacing them while UIKit animates an | 295 | + // UIButton custom views on iOS 26). Replacing them while UIKit animates an |
| 250 | + // existing one (menu capsule morph, push/pop glass transitions) strands the | 296 | + // existing one (menu capsule morph, push/pop glass transitions) strands the |
| 251 | + // animation overlay — a stuck expanded capsule or an unmasked square back | 297 | + // animation overlay — a stuck expanded capsule or an unmasked square back |
| 252 | + // button. When the JS configs are unchanged, keep the already-applied items. | 298 | + // button. When both the JS configs and React-backed item identities are |
| 253 | + // Subview-backed items are re-derived every pass, so their presence forces | 299 | + // unchanged, keep the already-applied item groups. Item groups are only |
| 254 | + // the rebuild path. | 300 | + // available from iOS 16, so older platforms keep the legacy rebuild path. |
| 255 | + BOOL reuseHeaderBarButtonItems = subviewLeftItems.count == 0 && subviewRightItems.count == 0 && | 301 | + BOOL canReuseHeaderBarButtonItems = NO; |
| | 302 | +#if !TARGET_OS_TV |
| | 303 | + if (@available(iOS 16.0, *)) { |
| | 304 | + canReuseHeaderBarButtonItems = YES; |
| | 305 | + } |
| | 306 | +#endif |
| | 307 | + BOOL reuseHeaderBarButtonItems = canReuseHeaderBarButtonItems && |
| 256 | + [objc_getAssociatedObject(navitem, &RNSAppliedHeaderBarButtonConfigsKey) isEqual:headerItemsKey]; | 308 | + [objc_getAssociatedObject(navitem, &RNSAppliedHeaderBarButtonConfigsKey) isEqual:headerItemsKey]; |
| 257 | + if (!reuseHeaderBarButtonItems) { | 309 | + if (!reuseHeaderBarButtonItems) { |
| 258 | + NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems | 310 | + NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems |
| 259 | + withCurrentItems:navitem.leftBarButtonItems | 311 | + withCurrentItems:subviewLeftItems |
| 260 | + navigationItem:navitem]; | 312 | + navigationItem:navitem]; |
| 261 | + NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems | 313 | + NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems |
| 262 | + withCurrentItems:navitem.rightBarButtonItems | 314 | + withCurrentItems:subviewRightItems |
| 263 | + navigationItem:navitem]; | 315 | + navigationItem:navitem]; |
| 264 | + NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems | 316 | + NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems |
| 265 | + withCurrentItems:@[] | 317 | + withCurrentItems:@[] |
| ... | @@ -271,8 +323,6 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -271,8 +323,6 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 271 | + if (@available(iOS 26.0, *)) { | 323 | + if (@available(iOS 26.0, *)) { |
| 272 | + navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems]; | 324 | + navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems]; |
| 273 | + } | 325 | + } |
| 274 | + navitem.leftBarButtonItems = nil; | | |
| 275 | + navitem.rightBarButtonItems = nil; | | |
| 276 | + } else { | 326 | + } else { |
| 277 | + navitem.leftBarButtonItems = leftBarButtonItems; | 327 | + navitem.leftBarButtonItems = leftBarButtonItems; |
| 278 | + navitem.rightBarButtonItems = rightBarButtonItems; | 328 | + navitem.rightBarButtonItems = rightBarButtonItems; |
| ... | @@ -281,13 +331,10 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -281,13 +331,10 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 281 | + navitem.leftBarButtonItems = leftBarButtonItems; | 331 | + navitem.leftBarButtonItems = leftBarButtonItems; |
| 282 | + navitem.rightBarButtonItems = rightBarButtonItems; | 332 | + navitem.rightBarButtonItems = rightBarButtonItems; |
| 283 | +#endif | 333 | +#endif |
| 284 | + // Only dict-driven items can be reused: with subview-backed items in the | | |
| 285 | + // mix the applied state depends on view identity, so clear the key to | | |
| 286 | + // force a rebuild on the next pass. | | |
| 287 | + objc_setAssociatedObject( | 334 | + objc_setAssociatedObject( |
| 288 | + navitem, | 335 | + navitem, |
| 289 | + &RNSAppliedHeaderBarButtonConfigsKey, | 336 | + &RNSAppliedHeaderBarButtonConfigsKey, |
| 290 | + subviewLeftItems.count == 0 && subviewRightItems.count == 0 ? headerItemsKey : nil, | 337 | + headerItemsKey, |
| 291 | + OBJC_ASSOCIATION_RETAIN_NONATOMIC); | 338 | + OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| 292 | + } | 339 | + } |
| 293 | + NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil; | 340 | + NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil; |
| ... | @@ -689,7 +736,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -689,7 +736,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 689 | | 736 | |
| 690 | // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items | 737 | // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items |
| 691 | // (setting nav bar visibility should be done after `navitem.*BarButtonItems`). | 738 | // (setting nav bar visibility should be done after `navitem.*BarButtonItems`). |
| 692 | @@ -773,6 +1259,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * | 739 | @@ -773,6 +1262,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 693 | | 740 | |
| 694 | - (NSArray<UIBarButtonItem *> *)barButtonItemsFromConfigs:(NSArray<NSDictionary<NSString *, id> *> *)dicts | 741 | - (NSArray<UIBarButtonItem *> *)barButtonItemsFromConfigs:(NSArray<NSDictionary<NSString *, id> *> *)dicts |
| 695 | withCurrentItems:(NSArray<UIBarButtonItem *> *)currentItems | 742 | withCurrentItems:(NSArray<UIBarButtonItem *> *)currentItems |
| ... | @@ -697,7 +744,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -697,7 +744,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 697 | { | 744 | { |
| 698 | if (dicts.count == 0) { | 745 | if (dicts.count == 0) { |
| 699 | return currentItems; | 746 | return currentItems; |
| 700 | @@ -781,7 +1268,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * | 747 | @@ -781,7 +1271,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 701 | [items addObjectsFromArray:currentItems]; | 748 | [items addObjectsFromArray:currentItems]; |
| 702 | for (NSUInteger i = 0; i < dicts.count; i++) { | 749 | for (NSUInteger i = 0; i < dicts.count; i++) { |
| 703 | NSDictionary *dict = dicts[i]; | 750 | NSDictionary *dict = dicts[i]; |
| ... | @@ -896,7 +943,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -896,7 +943,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 896 | RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict | 943 | RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict |
| 897 | action:^(NSString *buttonId) { | 944 | action:^(NSString *buttonId) { |
| 898 | auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>( | 945 | auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>( |
| 899 | @@ -803,19 +1480,23 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * | 946 | @@ -803,19 +1483,23 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 900 | } | 947 | } |
| 901 | imageLoader:_imageLoader]; | 948 | imageLoader:_imageLoader]; |
| 902 | NSNumber *index = dict[@"index"]; | 949 | NSNumber *index = dict[@"index"]; |
| ... | @@ -926,7 +973,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -926,7 +973,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 926 | [items insertObject:item atIndex:index.integerValue]; | 973 | [items insertObject:item atIndex:index.integerValue]; |
| 927 | } else { | 974 | } else { |
| 928 | [items addObject:item]; | 975 | [items addObject:item]; |
| 929 | @@ -825,6 +1506,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * | 976 | @@ -825,6 +1509,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 930 | return items; | 977 | return items; |
| 931 | } | 978 | } |
| 932 | | 979 | |
| ... | @@ -974,7 +1021,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -974,7 +1021,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 974 | RNS_IGNORE_SUPER_CALL_BEGIN | 1021 | RNS_IGNORE_SUPER_CALL_BEGIN |
| 975 | - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex | 1022 | - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex |
| 976 | { | 1023 | { |
| 977 | @@ -1013,6 +1735,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: | 1024 | @@ -1013,6 +1738,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: |
| 978 | } | 1025 | } |
| 979 | | 1026 | |
| 980 | _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); | 1027 | _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); |
| ... | @@ -983,7 +1030,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -983,7 +1030,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 983 | if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { | 1030 | if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { |
| 984 | _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); | 1031 | _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); |
| 985 | } | 1032 | } |
| 986 | @@ -1038,6 +1762,7 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: | 1033 | @@ -1038,6 +1765,7 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: |
| 987 | _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; | 1034 | _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; |
| 988 | _backButtonDisplayMode = | 1035 | _backButtonDisplayMode = |
| 989 | [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; | 1036 | [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; |
| ... | @@ -991,7 +1038,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 | ... | @@ -991,7 +1038,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 991 | | 1038 | |
| 992 | if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { | 1039 | if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { |
| 993 | _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; | 1040 | _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; |
| 994 | @@ -1084,6 +1809,30 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: | 1041 | @@ -1084,6 +1812,30 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: |
| 995 | _headerRightBarButtonItems = array; | 1042 | _headerRightBarButtonItems = array; |
| 996 | } | 1043 | } |
| 997 | | 1044 | |