[iOS][SplitView] Switching the color to undefined leaves the blur effect behind sidebar
Description
We've identified a native-level bug that can be consistently reproduced when the following conditions are met:
- The deepest view in the hierarchy is a
Textcomponent. - The background color of the view is set to a value with
alpha = 0 - There is an odd number of parent views between the
Textcomponent and the root ofSplitViewScreenthat havecollapsable: false.
When all of the above conditions are true, the blur effect caused by the transparent background does not disappear under the primary column as expected.
Steps to reproduce
- Run the example from snack
- Click on some pressables - blur is left after unfocusing the element
https://github.com/user-attachments/assets/42049deb-738e-4d8f-8c2a-cfad2dc3d879
Snack or a link to a repository
https://snack.expo.dev/@tomasz.boron/curious-indigo-ramen
Screens version
4.17.1
React Native version
0.82.0
Platforms
iOS
JavaScript runtime
Hermes
Workflow
React Native (without Expo)
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
iOS simulator
Device model
No response
Acknowledgements
Yes
The issue is reproducible with the native application
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray<UIView *> *pressableViews;
@end
@implementation ViewController
#pragma mark - Init
- (instancetype)init {
self = [super initWithStyle:UISplitViewControllerStyleDoubleColumn];
if (self) {
[self configureSplitView];
}
return self;
}
- (void)configureSplitView {
self.pressableViews = [NSMutableArray array];
UIViewController *primaryVC = [[UIViewController alloc] init];
primaryVC.view.backgroundColor = UIColor.clearColor;
UIViewController *secondaryVC = [[UIViewController alloc] init];
secondaryVC.view.backgroundColor = UIColor.clearColor;
UIView *containerView = secondaryVC.view;
UIView *previousView = nil;
for (NSInteger i = 0; i < 4; i++) {
UIView *pressableView = [[UIView alloc] init];
pressableView.translatesAutoresizingMaskIntoConstraints = NO;
pressableView.backgroundColor = UIColor.clearColor;
pressableView.userInteractionEnabled = YES;
[containerView addSubview:pressableView];
[NSLayoutConstraint activateConstraints:@[
[pressableView.leadingAnchor constraintEqualToAnchor:containerView.leadingAnchor constant:16],
[pressableView.trailingAnchor constraintEqualToAnchor:containerView.trailingAnchor constant:-16],
[pressableView.heightAnchor constraintEqualToConstant:200]
]];
if (previousView == nil) {
[pressableView.topAnchor constraintEqualToAnchor:containerView.topAnchor constant:24].active = YES;
} else {
[pressableView.topAnchor constraintEqualToAnchor:previousView.bottomAnchor constant:16].active = YES;
}
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handlePress:)];
[pressableView addGestureRecognizer:tapRecognizer];
[self.pressableViews addObject:pressableView];
// uncomment to make it work
//
// UIView *extraView = [[UIView alloc] init];
// extraView.translatesAutoresizingMaskIntoConstraints = NO;
// extraView.backgroundColor = [UIColor clearColor];
// [pressableView addSubview:extraView];
//
// [NSLayoutConstraint activateConstraints:@[
// [extraView.leadingAnchor constraintEqualToAnchor:pressableView.leadingAnchor],
// [extraView.trailingAnchor constraintEqualToAnchor:pressableView.trailingAnchor],
// [extraView.bottomAnchor constraintEqualToAnchor:pressableView.bottomAnchor],
// [extraView.heightAnchor constraintEqualToConstant:50]
// ]];
previousView = pressableView;
}
if (previousView) {
[NSLayoutConstraint activateConstraints:@[
[previousView.bottomAnchor constraintLessThanOrEqualToAnchor:containerView.bottomAnchor constant:-24]
]];
}
[self setViewController:primaryVC forColumn:UISplitViewControllerColumnPrimary];
[self setViewController:secondaryVC forColumn:UISplitViewControllerColumnSecondary];
self.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary;
self.preferredSplitBehavior = UISplitViewControllerSplitBehaviorTile;
}
- (void)handlePress:(UITapGestureRecognizer *)gestureRecognizer {
UIView *pressedView = gestureRecognizer.view;
if (!pressedView) return;
for (UIView *view in self.pressableViews) {
view.backgroundColor = (view == pressedView) ? [UIColor magentaColor] : [UIColor clearColor];
}
}
@end
The issue occurs on the native side, as noted above, because UIKit incorrectly clears the blur on the appropriate layer. A trigger, such as focusing on the primary column, causes the view to be redrawn. Forcing a view redraw would be a viable workaround, but the correct solution should be applied on the native side. Any actions taken on our side will merely serve as workarounds, similar to those I have implemented natively.
At the moment, we’re unable to provide a valid workaround on the RNScreens side without causing significant performance regressions. However, we can suggest a few potential workarounds on the application side:
Add an artificial non-collapsable wrapper
Before
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow]}>
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>
After
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow]}>
<View collapsable={false}>
<View collapsable={false}> // inserting additional View, the number of non-collapsable views should be even
<Text>
{title}
</Text>
</View>
</View>
</Pressable>
Set a non-zero borderWidth on the view where background changes and blur issues occur
Before
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow]}>
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>
After
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow, {borderWidth: 1, borderColor: 'transparent'}]}> // add transparent border to styles
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>
Use a background color with a low alpha > 0 instead of clearColor, transparent, etc.
Before
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow]}>
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>
After
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive ? {backgroundColor: '#ff00ffff'} : {backgroundColor: '#ff00ff01'}]}> // add non-zero opacity color for inactive state
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>
Use shouldRasterizeIOS prop on the view setting the backgroundColor
Before
<Pressable
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow]}>
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>
After
<Pressable
shouldRasterizeIOS // toggle rasterization to rely on the bitmap from this view level
onPress={toggleBgColor}
style={[styles.row, isActive && styles.activeRow]}>
<View collapsable={false}>
<Text>
{title}
</Text>
</View>
</Pressable>