| ... | ... | @@ -226,7 +226,45 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 226 | 226 | |
| 227 | 227 | // appearance does not apply to the tvOS so we need to use lagacy customization |
| 228 | 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 | 268 | // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). |
| 231 | 269 | // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) |
| 232 | 270 | navitem.title = config.title; |
| ... | ... | @@ -237,29 +275,43 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 237 | 275 | + NSArray *headerLeftConfigs = config.headerLeftBarButtonItems ?: @[]; |
| 238 | 276 | + NSArray *headerRightConfigs = config.headerRightBarButtonItems ?: @[]; |
| 239 | 277 | + NSArray *headerCenterConfigs = config.headerCenterBarButtonItems ?: @[]; |
| 240 | | + NSArray<UIBarButtonItem *> *subviewLeftItems = navitem.leftBarButtonItems ?: @[]; |
| 241 | | + NSArray<UIBarButtonItem *> *subviewRightItems = navitem.rightBarButtonItems ?: @[]; |
| 242 | | + // The key includes the config instance's identity: cached items capture |
| 243 | | + // this config's event emitter in their press handlers, so a remounted |
| 244 | | + // header-config view with value-equal configs must still rebuild — reusing |
| 245 | | + // the old items would dispatch presses into the dead config's emitter. |
| 246 | | + NSArray *headerItemsKey = |
| 247 | | + @[ @((uintptr_t)config), headerLeftConfigs, headerRightConfigs, headerCenterConfigs ]; |
| 278 | + // Retain the config in the key so its address cannot be reused while cached. |
| 279 | + // Items capture this config's event emitter in their press handlers, so a |
| 280 | + // remounted header-config view with value-equal configs must still rebuild |
| 281 | + // instead of dispatching presses into the old config's emitter. |
| 282 | + // getUIBarButtonItem is stable for the lifetime of its React header |
| 283 | + // subview. Include those native identities in the key so React-backed |
| 284 | + // items can use the same cache without reusing a group after its view was |
| 285 | + // replaced. |
| 286 | + NSArray *headerItemsKey = @[ |
| 287 | + config, |
| 288 | + headerLeftConfigs, |
| 289 | + headerRightConfigs, |
| 290 | + headerCenterConfigs, |
| 291 | + subviewLeftItems, |
| 292 | + subviewRightItems, |
| 293 | + ]; |
| 248 | 294 | + // Rebuilding bar button items creates brand-new native buttons (glass |
| 249 | 295 | + // UIButton custom views on iOS 26). Replacing them while UIKit animates an |
| 250 | 296 | + // existing one (menu capsule morph, push/pop glass transitions) strands the |
| 251 | 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. |
| 253 | | + // Subview-backed items are re-derived every pass, so their presence forces |
| 254 | | + // the rebuild path. |
| 255 | | + BOOL reuseHeaderBarButtonItems = subviewLeftItems.count == 0 && subviewRightItems.count == 0 && |
| 298 | + // button. When both the JS configs and React-backed item identities are |
| 299 | + // unchanged, keep the already-applied item groups. Item groups are only |
| 300 | + // available from iOS 16, so older platforms keep the legacy rebuild path. |
| 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 | 308 | + [objc_getAssociatedObject(navitem, &RNSAppliedHeaderBarButtonConfigsKey) isEqual:headerItemsKey]; |
| 257 | 309 | + if (!reuseHeaderBarButtonItems) { |
| 258 | 310 | + NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems |
| 259 | | + withCurrentItems:navitem.leftBarButtonItems |
| 311 | + withCurrentItems:subviewLeftItems |
| 260 | 312 | + navigationItem:navitem]; |
| 261 | 313 | + NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems |
| 262 | | + withCurrentItems:navitem.rightBarButtonItems |
| 314 | + withCurrentItems:subviewRightItems |
| 263 | 315 | + navigationItem:navitem]; |
| 264 | 316 | + NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems |
| 265 | 317 | + withCurrentItems:@[] |
| ... | ... | @@ -271,8 +323,6 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 271 | 323 | + if (@available(iOS 26.0, *)) { |
| 272 | 324 | + navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems]; |
| 273 | 325 | + } |
| 274 | | + navitem.leftBarButtonItems = nil; |
| 275 | | + navitem.rightBarButtonItems = nil; |
| 276 | 326 | + } else { |
| 277 | 327 | + navitem.leftBarButtonItems = leftBarButtonItems; |
| 278 | 328 | + navitem.rightBarButtonItems = rightBarButtonItems; |
| ... | ... | @@ -281,13 +331,10 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 281 | 331 | + navitem.leftBarButtonItems = leftBarButtonItems; |
| 282 | 332 | + navitem.rightBarButtonItems = rightBarButtonItems; |
| 283 | 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 | 334 | + objc_setAssociatedObject( |
| 288 | 335 | + navitem, |
| 289 | 336 | + &RNSAppliedHeaderBarButtonConfigsKey, |
| 290 | | + subviewLeftItems.count == 0 && subviewRightItems.count == 0 ? headerItemsKey : nil, |
| 337 | + headerItemsKey, |
| 291 | 338 | + OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| 292 | 339 | + } |
| 293 | 340 | + NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil; |
| ... | ... | @@ -689,7 +736,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 689 | 736 | |
| 690 | 737 | // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items |
| 691 | 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 | 741 | - (NSArray<UIBarButtonItem *> *)barButtonItemsFromConfigs:(NSArray<NSDictionary<NSString *, id> *> *)dicts |
| 695 | 742 | withCurrentItems:(NSArray<UIBarButtonItem *> *)currentItems |
| ... | ... | @@ -697,7 +744,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 697 | 744 | { |
| 698 | 745 | if (dicts.count == 0) { |
| 699 | 746 | return currentItems; |
| 700 | | @@ -781,7 +1268,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 747 | @@ -781,7 +1271,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 701 | 748 | [items addObjectsFromArray:currentItems]; |
| 702 | 749 | for (NSUInteger i = 0; i < dicts.count; i++) { |
| 703 | 750 | NSDictionary *dict = dicts[i]; |
| ... | ... | @@ -896,7 +943,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 896 | 943 | RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict |
| 897 | 944 | action:^(NSString *buttonId) { |
| 898 | 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 | 948 | imageLoader:_imageLoader]; |
| 902 | 949 | NSNumber *index = dict[@"index"]; |
| ... | ... | @@ -926,7 +973,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 926 | 973 | [items insertObject:item atIndex:index.integerValue]; |
| 927 | 974 | } else { |
| 928 | 975 | [items addObject:item]; |
| 929 | | @@ -825,6 +1506,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 976 | @@ -825,6 +1509,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * |
| 930 | 977 | return items; |
| 931 | 978 | } |
| 932 | 979 | |
| ... | ... | @@ -974,7 +1021,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 974 | 1021 | RNS_IGNORE_SUPER_CALL_BEGIN |
| 975 | 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 | 1027 | _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); |
| ... | ... | @@ -983,7 +1030,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 983 | 1030 | if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { |
| 984 | 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 | 1034 | _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; |
| 988 | 1035 | _backButtonDisplayMode = |
| 989 | 1036 | [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; |
| ... | ... | @@ -991,7 +1038,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb21 |
| 991 | 1038 | |
| 992 | 1039 | if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { |
| 993 | 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 | 1042 | _headerRightBarButtonItems = array; |
| 996 | 1043 | } |
| 997 | 1044 | |