Skip to content

feat(iOS) - blur filter using SwiftUI #52495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ let reactOSCompat = RNTarget(
path: "ReactCommon/oscompat"
)

let rctSwiftUI = RNTarget(
name: .rctSwiftUI,
path: "ReactApple/RCTSwiftUI"
)

let rctSwiftUIWrapper = RNTarget(
name: .rctSwiftUIWrapper,
path: "ReactApple/RCTSwiftUIWrapper",
dependencies: [.rctSwiftUI]
)

// React-rendererconsistency.podspec
let reactRendererConsistency = RNTarget(
name: .reactRendererConsistency,
Expand Down Expand Up @@ -417,7 +428,7 @@ let reactFabric = RNTarget(
let reactRCTFabric = RNTarget(
name: .reactRCTFabric,
path: "React/Fabric",
dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricComponents, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage]
dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricComponents, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper]
)

/// React-FabricComponents.podspec
Expand Down Expand Up @@ -556,6 +567,8 @@ let targets = [
reactCore,
reactCoreRCTWebsocket,
reactFabric,
rctSwiftUI,
rctSwiftUIWrapper,
reactRCTFabric,
reactFabricComponents,
reactFabricImage,
Expand Down Expand Up @@ -703,6 +716,9 @@ extension String {
static let logger = "React-logger"
static let mapbuffer = "React-Mapbuffer"

static let rctSwiftUI = "RCTSwiftUI"
static let rctSwiftUIWrapper = "RCTSwiftUIWrapper"

static let rctDeprecation = "RCT-Deprecation"
static let yoga = "Yoga"
static let reactUtils = "React-utils"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#import <React/RCTLinearGradient.h>
#import <React/RCTLocalizedString.h>
#import <React/RCTRadialGradient.h>
#import <RCTSwiftUIWrapper/RCTSwiftUIContainerViewWrapper.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/view/ViewComponentDescriptor.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
Expand Down Expand Up @@ -50,6 +51,7 @@ @implementation RCTViewComponentView {
UIView *_containerView;
BOOL _useCustomContainerView;
NSMutableSet<NSString *> *_accessibilityOrderNativeIDs;
RCTSwiftUIContainerViewWrapper* _swiftUIWrapper;
}

#ifdef RCT_DYNAMIC_FRAMEWORKS
Expand Down Expand Up @@ -576,6 +578,10 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
auto newTransform = _props->resolveTransform(layoutMetrics);
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
}

if (_swiftUIWrapper) {
[_swiftUIWrapper updateLayoutWithBounds:self.bounds];
}
}

- (BOOL)isJSResponder
Expand Down Expand Up @@ -793,44 +799,96 @@ - (BOOL)styleWouldClipOverflowInk
((!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth)) || _props->outlineWidth != 0);
}

// The view that is used as the receiver for all styling (borders, background,
// etc.). Most of the time, this is just `self`. When a view has a filter like
// `blur` applied, we need to wrap it in a SwiftUI view to render the effect.
// In this case, `effectiveContentView` will be the content view inside the
// SwiftUI wrapper.
- (UIView *)effectiveContentView
{
if (!ReactNativeFeatureFlags::enableSwiftUIBasedFilters()) {
return self;
}

UIView *effectiveContentView = self;

if (self.styleNeedsSwiftUIContainer) {
if (!_swiftUIWrapper) {
_swiftUIWrapper = [RCTSwiftUIContainerViewWrapper new];
UIView *swiftUIContentView = [[UIView alloc] init];
for (UIView *subview in self.subviews) {
[swiftUIContentView addSubview:subview];
}
swiftUIContentView.clipsToBounds = self.clipsToBounds;
self.clipsToBounds = NO;
swiftUIContentView.layer.mask = self.layer.mask;
self.layer.mask = nil;
[_swiftUIWrapper updateContentView:swiftUIContentView];
[_swiftUIWrapper updateLayoutWithBounds:self.bounds];
[self addSubview:_swiftUIWrapper.hostingView];

[self transferVisualPropertiesFromView:self toView:swiftUIContentView];
}

effectiveContentView = _swiftUIWrapper.contentView;
} else {
if (_swiftUIWrapper) {
UIView *swiftUIContentView = _swiftUIWrapper.contentView;
for (UIView *subview in swiftUIContentView.subviews) {
[self addSubview:subview];
}
self.clipsToBounds = swiftUIContentView.clipsToBounds;
self.layer.mask = swiftUIContentView.layer.mask;

[self transferVisualPropertiesFromView:swiftUIContentView toView:self];

[_swiftUIWrapper.hostingView removeFromSuperview];
_swiftUIWrapper = nil;
}
}

return effectiveContentView;
}

// This UIView is the UIView that holds all subviews. It is sometimes not self
// because we want to render "overflow ink" that extends beyond the bounds of
// the view and is not affected by clipping.
- (UIView *)currentContainerView
{
UIView* effectiveContentView = self.effectiveContentView;

if (_useCustomContainerView) {
if (!_containerView) {
_containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
for (UIView *subview in self.subviews) {
for (UIView *subview in effectiveContentView.subviews) {
[_containerView addSubview:subview];
}
_containerView.clipsToBounds = self.clipsToBounds;
self.clipsToBounds = NO;
_containerView.layer.mask = self.layer.mask;
self.layer.mask = nil;
[self addSubview:_containerView];
_containerView.clipsToBounds = effectiveContentView.clipsToBounds;
effectiveContentView.clipsToBounds = NO;
_containerView.layer.mask = effectiveContentView.layer.mask;
effectiveContentView.layer.mask = nil;
[effectiveContentView addSubview:_containerView];
}

return _containerView;
effectiveContentView = _containerView;
} else {
if (_containerView) {
for (UIView *subview in _containerView.subviews) {
[self addSubview:subview];
[effectiveContentView addSubview:subview];
}
self.clipsToBounds = _containerView.clipsToBounds;
self.layer.mask = _containerView.layer.mask;
effectiveContentView.clipsToBounds = _containerView.clipsToBounds;
effectiveContentView.layer.mask = _containerView.layer.mask;
[_containerView removeFromSuperview];
_containerView = nil;
}

return self;
}
return effectiveContentView;
}

- (void)invalidateLayer
{
CALayer *layer = self.layer;

CALayer *layer = self.effectiveContentView.layer;
if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
return;
}
Expand Down Expand Up @@ -910,7 +968,7 @@ - (void)invalidateLayer
if (!_backgroundColorLayer) {
_backgroundColorLayer = [CALayer layer];
_backgroundColorLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
[self.layer addSublayer:_backgroundColorLayer];
[layer addSublayer:_backgroundColorLayer];
}
[self shapeLayerToMatchView:_backgroundColorLayer borderMetrics:borderMetrics];
_backgroundColorLayer.backgroundColor = backgroundColor.CGColor;
Expand Down Expand Up @@ -986,31 +1044,43 @@ - (void)invalidateLayer
// filter
[_filterLayer removeFromSuperlayer];
_filterLayer = nil;
if (_swiftUIWrapper) {
[_swiftUIWrapper updateBlurRadius:@(0)];
}
self.layer.opacity = (float)_props->opacity;
if (!_props->filter.empty()) {
float multiplicativeBrightness = 1;
bool hasBrightnessFilter = false;
for (const auto &primitive : _props->filter) {
if (std::holds_alternative<Float>(primitive.parameters)) {
if (primitive.type == FilterType::Brightness) {
multiplicativeBrightness *= std::get<Float>(primitive.parameters);
hasBrightnessFilter = true;
} else if (primitive.type == FilterType::Opacity) {
self.layer.opacity *= std::get<Float>(primitive.parameters);
} else if (primitive.type == FilterType::Blur) {
if (_swiftUIWrapper) {
Float blurRadius = std::get<Float>(primitive.parameters);
[_swiftUIWrapper updateBlurRadius:@(blurRadius)];
}
}
}
}

_filterLayer = [CALayer layer];
[self shapeLayerToMatchView:_filterLayer borderMetrics:borderMetrics];
_filterLayer.compositingFilter = @"multiplyBlendMode";
_filterLayer.backgroundColor = [UIColor colorWithRed:multiplicativeBrightness
green:multiplicativeBrightness
blue:multiplicativeBrightness
alpha:self.layer.opacity]
.CGColor;
// So that this layer is always above any potential sublayers this view may
// add
_filterLayer.zPosition = CGFLOAT_MAX;
[self.layer addSublayer:_filterLayer];

if (hasBrightnessFilter) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will make another PR to move brightness filter + add additional filters with SwiftUI.

_filterLayer = [CALayer layer];
[self shapeLayerToMatchView:_filterLayer borderMetrics:borderMetrics];
_filterLayer.compositingFilter = @"multiplyBlendMode";
_filterLayer.backgroundColor = [UIColor colorWithRed:multiplicativeBrightness
green:multiplicativeBrightness
blue:multiplicativeBrightness
alpha:self.layer.opacity]
.CGColor;
// So that this layer is always above any potential sublayers this view may
// add
_filterLayer.zPosition = CGFLOAT_MAX;
[layer addSublayer:_filterLayer];
}
}

// background image
Expand All @@ -1025,7 +1095,7 @@ - (void)invalidateLayer
[self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics];
backgroundImageLayer.masksToBounds = YES;
backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
[self.layer addSublayer:backgroundImageLayer];
[layer addSublayer:backgroundImageLayer];
[_backgroundImageLayers addObject:backgroundImageLayer];
} else if (std::holds_alternative<RadialGradient>(backgroundImage)) {
const auto &radialGradient = std::get<RadialGradient>(backgroundImage);
Expand All @@ -1034,7 +1104,7 @@ - (void)invalidateLayer
[self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics];
backgroundImageLayer.masksToBounds = YES;
backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
[self.layer addSublayer:backgroundImageLayer];
[layer addSublayer:backgroundImageLayer];
[_backgroundImageLayers addObject:backgroundImageLayer];
}
}
Expand All @@ -1056,7 +1126,7 @@ - (void)invalidateLayer
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
self.layer.bounds.size);
shadowLayer.zPosition = _borderLayer.zPosition;
[self.layer addSublayer:shadowLayer];
[layer addSublayer:shadowLayer];
[_boxShadowLayers addObject:shadowLayer];
}
}
Expand Down Expand Up @@ -1410,6 +1480,63 @@ - (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
return RCTNSStringFromString([[self class] componentDescriptorProvider].name);
}

- (BOOL) styleNeedsSwiftUIContainer {
for (const auto &primitive : _props->filter) {
if (primitive.type == FilterType::Blur) {
return YES;
}
}
return NO;
}

- (void)transferVisualPropertiesFromView:(UIView *)sourceView toView:(UIView *)destinationView
{
// shadow
destinationView.layer.shadowColor = sourceView.layer.shadowColor;
sourceView.layer.shadowColor = nil;
destinationView.layer.shadowOffset = sourceView.layer.shadowOffset;
sourceView.layer.shadowOffset = CGSizeZero;
destinationView.layer.shadowOpacity = sourceView.layer.shadowOpacity;
sourceView.layer.shadowOpacity = 0;
destinationView.layer.shadowRadius = sourceView.layer.shadowRadius;
sourceView.layer.shadowRadius = 0;

// background
destinationView.layer.backgroundColor = sourceView.layer.backgroundColor;
sourceView.layer.backgroundColor = nil;
if (_backgroundColorLayer) {
[destinationView.layer addSublayer:_backgroundColorLayer];
}

// border
destinationView.layer.borderColor = sourceView.layer.borderColor;
sourceView.layer.borderColor = nil;
destinationView.layer.borderWidth = sourceView.layer.borderWidth;
sourceView.layer.borderWidth = 0;

// corner
destinationView.layer.cornerRadius = sourceView.layer.cornerRadius;
sourceView.layer.cornerRadius = 0;
destinationView.layer.cornerCurve = sourceView.layer.cornerCurve;

// custom layers
if (_borderLayer) {
[destinationView.layer addSublayer:_borderLayer];
}
if (_outlineLayer) {
[destinationView.layer addSublayer:_outlineLayer];
}
if (_filterLayer) {
[destinationView.layer addSublayer:_filterLayer];
}
for (CALayer *layer in _backgroundImageLayers) {
[destinationView.layer addSublayer:layer];
}
for (CALayer *layer in _boxShadowLayers) {
[destinationView.layer addSublayer:layer];
}
}

@end

#ifdef __cplusplus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<2294f3350aca0f19862f8cfdbe9479b6>>
* @generated SignedSource<<8c98913f6aa27523fcef5b4e86aee818>>
*/

/**
Expand Down Expand Up @@ -252,6 +252,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableResourceTimingAPI(): Boolean = accessor.enableResourceTimingAPI()

/**
* When enabled, it will use SwiftUI for filter effects like blur on iOS.
*/
@JvmStatic
public fun enableSwiftUIBasedFilters(): Boolean = accessor.enableSwiftUIBasedFilters()

/**
* Enables View Culling: as soon as a view goes off screen, it can be reused anywhere in the UI and pieced together with other items to create new UI elements.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9f50b2fc5f4aad27e6cd8ecbde3d791a>>
* @generated SignedSource<<dc30f59142a3b995e4e904215e2899b1>>
*/

/**
Expand Down Expand Up @@ -57,6 +57,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableResourceTimingAPICache: Boolean? = null
private var enableSwiftUIBasedFiltersCache: Boolean? = null
private var enableViewCullingCache: Boolean? = null
private var enableViewRecyclingCache: Boolean? = null
private var enableViewRecyclingForTextCache: Boolean? = null
Expand Down Expand Up @@ -422,6 +423,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableSwiftUIBasedFilters(): Boolean {
var cached = enableSwiftUIBasedFiltersCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableSwiftUIBasedFilters()
enableSwiftUIBasedFiltersCache = cached
}
return cached
}

override fun enableViewCulling(): Boolean {
var cached = enableViewCullingCache
if (cached == null) {
Expand Down
Loading
Loading