Skip to content

Commit 1198a55

Browse files
Add grayscale, drop-shadow and saturate filter support on iOS (#53873)
Summary: This PR adds support for `grayscale`, `drop-shadow` and `saturate` filter support on iOS as discussed here - react-native-community/discussions-and-proposals#927 ## Changelog: [IOS] [ADDED] - Add grayscale, drop-shadow and saturate CSS filters <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #53873 Test Plan: Test Filter screen in RNTester app. Checkout `grayscale`, `drop-shadow` and `saturate` examples. Results are consistent on android and iOS. <img width="auto" height="300" alt="Screenshot 2025-09-22 at 1 07 28 PM" src="https://github.com/user-attachments/assets/f26fc60a-48f6-4aa2-82e9-56e986082fed" /> <img width="auto" height="300" alt="Screenshot 2025-09-22 at 1 08 08 PM" src="https://github.com/user-attachments/assets/d87925c8-f05c-4ed2-a651-8c7670339f87" /> <img width="auto" height="300" alt="Screenshot 2025-09-22 at 2 37 30 PM" src="https://github.com/user-attachments/assets/582fcadf-1189-4f01-9868-ff9fe71eeda0" /> ## Aside Will do one more PR to add remaining filters. Splitting into two to keep it easier to review. Reviewed By: cipolleschi, jorge-cab Differential Revision: D85352008 Pulled By: joevilches fbshipit-source-id: 33931ceeaa6f47cf39988c8696bd90f659ade730
1 parent f9e3db5 commit 1198a55

5 files changed

Lines changed: 79 additions & 6 deletions

File tree

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,14 +1051,23 @@ - (void)invalidateLayer
10511051
[_filterLayer removeFromSuperlayer];
10521052
_filterLayer = nil;
10531053
if (_swiftUIWrapper != nullptr) {
1054-
[_swiftUIWrapper updateBlurRadius:@(0)];
1054+
[_swiftUIWrapper resetStyles];
10551055
}
10561056
self.layer.opacity = (float)_props->opacity;
10571057
if (!_props->filter.empty()) {
10581058
float multiplicativeBrightness = 1;
10591059
bool hasBrightnessFilter = false;
10601060
for (const auto &primitive : _props->filter) {
1061-
if (std::holds_alternative<Float>(primitive.parameters)) {
1061+
if (primitive.type == FilterType::DropShadow) {
1062+
if (_swiftUIWrapper != nullptr && std::holds_alternative<DropShadowParams>(primitive.parameters)) {
1063+
const auto &dropShadowParams = std::get<DropShadowParams>(primitive.parameters);
1064+
UIColor *shadowColor = RCTUIColorFromSharedColor(dropShadowParams.color);
1065+
[_swiftUIWrapper updateDropShadow:@(dropShadowParams.standardDeviation)
1066+
x:@(dropShadowParams.offsetX)
1067+
y:@(dropShadowParams.offsetY)
1068+
color:shadowColor];
1069+
}
1070+
} else if (std::holds_alternative<Float>(primitive.parameters)) {
10621071
if (primitive.type == FilterType::Brightness) {
10631072
multiplicativeBrightness *= std::get<Float>(primitive.parameters);
10641073
hasBrightnessFilter = true;
@@ -1069,6 +1078,16 @@ - (void)invalidateLayer
10691078
Float blurRadius = std::get<Float>(primitive.parameters);
10701079
[_swiftUIWrapper updateBlurRadius:@(blurRadius)];
10711080
}
1081+
} else if (primitive.type == FilterType::Grayscale) {
1082+
if (_swiftUIWrapper != nullptr) {
1083+
Float grayscale = std::get<Float>(primitive.parameters);
1084+
[_swiftUIWrapper updateGrayscale:@(grayscale)];
1085+
}
1086+
} else if (primitive.type == FilterType::Saturate) {
1087+
if (_swiftUIWrapper != nullptr) {
1088+
Float saturation = std::get<Float>(primitive.parameters);
1089+
[_swiftUIWrapper updateSaturation:@(saturation)];
1090+
}
10721091
}
10731092
}
10741093
}
@@ -1525,7 +1544,8 @@ - (BOOL)styleNeedsSwiftUIContainer
15251544
{
15261545
if (!_props->filter.empty()) {
15271546
for (const auto &primitive : _props->filter) {
1528-
if (primitive.type == FilterType::Blur) {
1547+
if (primitive.type == FilterType::Blur || primitive.type == FilterType::Grayscale ||
1548+
primitive.type == FilterType::DropShadow || primitive.type == FilterType::Saturate) {
15291549
return YES;
15301550
}
15311551
}

packages/react-native/ReactApple/RCTSwiftUI/RCTSwiftUIContainerView.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,53 @@ import UIKit
3838
containerViewModel.blurRadius = blurRadius
3939
}
4040

41+
@objc public func updateGrayscale(_ grayscale: NSNumber) {
42+
containerViewModel.grayscale = CGFloat(grayscale.floatValue)
43+
}
44+
45+
@objc public func updateDropShadow(standardDeviation: NSNumber, x: NSNumber, y: NSNumber, color: UIColor) {
46+
containerViewModel.shadowRadius = CGFloat(standardDeviation.floatValue)
47+
containerViewModel.shadowX = CGFloat(x.floatValue)
48+
containerViewModel.shadowY = CGFloat(y.floatValue)
49+
containerViewModel.shadowColor = Color(color)
50+
}
51+
52+
@objc public func updateSaturation(_ saturation: NSNumber) {
53+
containerViewModel.saturationAmount = CGFloat(saturation.floatValue)
54+
}
55+
4156
@objc public func updateLayout(withBounds bounds: CGRect) {
4257
hostingController?.view.frame = bounds
4358
containerViewModel.contentView?.frame = bounds
4459
}
4560

4661
@objc public func resetStyles() {
4762
containerViewModel.blurRadius = 0
63+
containerViewModel.grayscale = 0
64+
containerViewModel.shadowRadius = 0
65+
containerViewModel.shadowX = 0
66+
containerViewModel.shadowY = 0
67+
containerViewModel.shadowColor = Color.clear
68+
containerViewModel.saturationAmount = 1
4869
}
4970
}
5071

5172
class ContainerViewModel: ObservableObject {
73+
// blur filter properties
5274
@Published var blurRadius: CGFloat = 0
75+
76+
// grayscale filter properties
77+
@Published var grayscale: CGFloat = 0
78+
79+
// drop-shadow filter properties
80+
@Published var shadowRadius: CGFloat = 0
81+
@Published var shadowX: CGFloat = 0
82+
@Published var shadowY: CGFloat = 0
83+
@Published var shadowColor: Color = Color.clear
84+
85+
// saturation filter properties
86+
@Published var saturationAmount: CGFloat = 1
87+
5388
@Published var contentView: UIView?
5489
}
5590

@@ -60,6 +95,9 @@ struct SwiftUIContainerView: View {
6095
if let contentView = viewModel.contentView {
6196
UIViewWrapper(view: contentView)
6297
.blur(radius: viewModel.blurRadius)
98+
.grayscale(viewModel.grayscale)
99+
.shadow(color: viewModel.shadowColor, radius: viewModel.shadowRadius, x: viewModel.shadowX, y: viewModel.shadowY)
100+
.saturation(viewModel.saturationAmount)
63101
}
64102
}
65103
}

packages/react-native/ReactApple/RCTSwiftUIWrapper/RCTSwiftUIContainerViewWrapper.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
1414

1515
- (UIView *_Nullable)contentView;
1616
- (void)updateBlurRadius:(NSNumber *)radius;
17+
- (void)updateGrayscale:(NSNumber *)grayscale;
18+
- (void)updateDropShadow:(NSNumber *)standardDeviation x:(NSNumber *)x y:(NSNumber *)y color:(UIColor *)color;
19+
- (void)updateSaturation:(NSNumber *)saturation;
1720
- (void)updateContentView:(UIView *)view;
1821
- (UIView *_Nullable)hostingView;
1922
- (void)resetStyles;

packages/react-native/ReactApple/RCTSwiftUIWrapper/RCTSwiftUIContainerViewWrapper.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,21 @@ - (void)updateBlurRadius:(NSNumber *)radius
4848
[self.swiftContainerView updateBlurRadius:radius];
4949
}
5050

51+
- (void)updateGrayscale:(NSNumber *)grayscale
52+
{
53+
[self.swiftContainerView updateGrayscale:grayscale];
54+
}
55+
56+
- (void)updateSaturation:(NSNumber *)saturation
57+
{
58+
[self.swiftContainerView updateSaturation:saturation];
59+
}
60+
61+
- (void)updateDropShadow:(NSNumber *)standardDeviation x:(NSNumber *)x y:(NSNumber *)y color:(UIColor *)color
62+
{
63+
[self.swiftContainerView updateDropShadowWithStandardDeviation:standardDeviation x:x y:y color:color];
64+
}
65+
5166
- (void)updateLayoutWithBounds:(CGRect)bounds
5267
{
5368
[self.swiftContainerView updateLayoutWithBounds:bounds];

packages/rn-tester/js/examples/Filter/FilterExample.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ exports.examples = [
155155
title: 'Grayscale',
156156
description: 'grayscale(0.5)',
157157
name: 'grayscale',
158-
platform: 'android',
159158
render(): React.Node {
160159
return (
161160
<StaticViewAndImageComparison style={{filter: [{grayscale: 0.5}]}} />
@@ -166,7 +165,6 @@ exports.examples = [
166165
title: 'Saturate',
167166
description: 'saturate(4)',
168167
name: 'saturate',
169-
platform: 'android',
170168
render(): React.Node {
171169
return <StaticViewAndImageComparison style={{filter: [{saturate: 4}]}} />;
172170
},
@@ -231,7 +229,6 @@ exports.examples = [
231229
title: 'Drop Shadow',
232230
description: 'drop-shadow(30px 10px 4px #4444dd)',
233231
name: 'drop-shadow',
234-
platform: 'android',
235232
render(): React.Node {
236233
return (
237234
<StaticViewAndImageComparison

0 commit comments

Comments
 (0)