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 11 commits into
base: main
Choose a base branch
from

Conversation

intergalacticspacehighway
Copy link
Contributor

@intergalacticspacehighway intergalacticspacehighway commented Jul 9, 2025

Summary:

As per the discussion on the previous PR thread, this PR uses SwiftUI to implement blur filter on iOS.

Approach:

To implement blur filter on iOS, we have two options:

  1. Use CAFilter (private API, app can get rejected/API can break). Earlier PR was using that approach. Thanks to Nick for suggesting SwiftUI API.

  2. Use SwiftUI. Wrap the view in a SwiftUI view and apply blur. This PR builds on top of that approach. This also enables a way to add SwiftUI only features like this one. Additional filters (grayscale, saturate, contrast, hueRotate) can also be added.

There are a few ways we can implement the SwiftUI approach:

  1. Create a new RCTSwiftUIComponentView -> do style flattening in View -> check if filter is present and conditionally render the RCTSwiftUIComponentView on iOS, wrap children with a SwiftUI view. Tradeoff with this approach is that it adds StyleSheet.flatten overhead on JS side.
  2. Add a SwiftUI container view inside of RCTViewComponentView. Tradeoff with this approach is that it complicates RCTViewComponentView a bit.

I decided to go with 2 to avoid the flattening tradeoff and try to minimize complicating RCTViewComponentView. it only adds the wrapper if it's required and removes if not (in this PR, blur filter style will add the wrapper, it will get removed if blur filter styling gets removed). It uses the existing container view pattern.

Changelog:

[IOS][ADDED] - Filter blur

Test Plan:

Test filter blur example on iOS. SwiftUI view should be added to the hierarchy.

Aside:

  • This PR also adds a new swift podspec. Creating a new podspec felt the right approach as adding swift in existing ones were adding some complexity. But open for changes here. Also, need some eyes on the podspec configs. cc - @chrfalch 🙏 this might also affect the SPM migration.
  • Unrelated: Existing brightness filter has some inconsistency compared to android and web, it uses self.layer.opacity so transparent background color do not blend well unless the view has an opacity. One solution would be to calculate true background color by using brightness or else use the SwiftUI's brightness, which would be cleaner imo (tested and it works).

wip

somewhat working

working

fix additional stuff

update layout metrics

update blur radius

init

working

revert

use layer

reset

dkd

fix

final version

working

working

grayscale

parent frame set

swiftui container

wip

nit

nit

enable blur filter example

cleanup

renaming

cleanup

naming

remove unnecessary configs

nits

wip

nit

child mount working

remove needs swiftui container flag

working

nit

nit

nit

remove all animations

remove all animations

nit

working

refactor

nit

nit

fix brightness filter overlay

nit
@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Jul 9, 2025
@kacperkapusciak
Copy link
Contributor

Visual demo and additional context for This Week in React readers 😉

https://x.com/nishanbende/status/1941904475800891846

@nandorojo
Copy link

Thanks for continuing to push us forward, @intergalacticspacehighway.

@cipolleschi
Copy link
Contributor

@intergalacticspacehighway are you working directly with somebody in the team to have this pushed forward?
I was off for the last 10 days, so I might have missed some convos, but this change seems to me quite big:

  • several files have been touched
  • it introduces a completely new paradigm (SwiftUI)
  • still leverage Cocoapods and does not update the SwiftPM infra (which is, in fact, failing in CI)

Those kind of changes should usually go through some kind of process, like RFC or similar, with somebody at Meta championing it. Otherwise they are very hard to land.

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 9, 2025

@cipolleschi i have been chatting with @NickGerleman regarding SwiftUI here, also we were having some conversation over discord. The intention of the PR was not to introduce official SwiftUI support, but it is more of an implementation detail. It is touching 2 core files, others are completely new files or small configs. I will look into SwiftPM migration, but i wanted to discuss the approach before investing too much time into it and Nick has some context on it, also I tagged @chrfalch for SwiftPM concern. Will make sure to do an RFC next time! 🙏

@cipolleschi cipolleschi added the p: Expo Partner: Expo label Jul 9, 2025
@NickGerleman
Copy link
Contributor

I will try to take a look at this next week. This week has been an eventful oncall shift, so I am a bit backed up atm.

@NickGerleman
Copy link
Contributor

NickGerleman commented Jul 23, 2025

Sorry for the delay. Had a chance to go through all of this!

@joevilches will be a lot more familiar with how container view is set up. I know there was some intricacy, around how we set things up, depending on what we do with borders. I don't know if there is any way we can avoid the complexity with transferVisualPropertiesFromView, if we instead can add just another view to the hierarchy. For rare case where we have blur filters, we can make hierarchy larger I think. I think the added complexity to RCTViewComponentView is the thing that is spookiest with this change, that otherwise would be relatively safe, since we are not impacting existing view hierarchies.

A couple interesting scenarios came to mind, that would be helpful to validate explicitly:

  1. Do we correctly handle adding and removing blur filter? I can see logic there
  2. Is there any impact to things like hit testing within the filtered view?
  3. Do we have any examples with filters + transparent borders, which was IIRC the interesting container view case?

The other bit, around complexity, is that we now have a "filter layer" with logic only ever used for brightness, where we get creative with blend modes to implement that. But the SwiftUI approach is really generic, and as you mention, we could use the SwifUI filter for that.

Afaict, SwiftUI has built in support for hue-rotate, contrast, brightness, grayscale, etc. So we could use this approach to fully replace the filter layer, and delete that added complexity, so we "trade away" the complexity of the current approach, for the more generic SwitftUI filter view that could support all of the different filters. We would need a feature flag for this, but I really like the idea of being able to support everything, and having only one path for filter effects, that doesn't rely on creativity with blending. I can see you were starting to add grayscale there, so I am guessing you were going that path for the others, so I think the only real ask here, is setting this up, so we could delete the old filter path, and keep things simpler.

These all seem supported on iOS 13+ too, so we could fully support all the filters on iOS.

Concrete Steps: There are a couple interesting pieces of this. Can we split them up? E.g.

  1. If we need any custom build logic, starting with that, since we now have both the build systems to deal with like @cipolleschi was mentioning. Might even be easier to reuse existing targets.
  2. Let's add a feature flag, to enable SwiftUI based filters. This can be generated, by adding to ReactNativeFeatureFlags.config.js, and running yarn featureflags --update.
  3. Add the needed logic to use SwifUI container for brightness filters, instead of the current, gated in a way, where rolling out the SwifUI filters, would let us delete the current filter code
  4. Add the rest of the filters?

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 24, 2025

Thank you for reviewing @NickGerleman!

transferVisualPropertiesFromView, if we instead can add just another view to the hierarchy....

We need to remove the visual styles from parent view's layer or else they blend and make blur on child look weird. So i thought simplest way would be to copy the visual styles to SwiftUI content view layer. I also tried making a visual only layer and then just togging them between SwiftUI container and main container when blur gets added/removed. But this was adding more complexity.
The cleanest approach imo would be to use a new RCTViewComponentView inherited view which can abstract the layer logic so we don't touch the original RCTViewComponentView but that requires adding flatten style call in JS 😓 (i tried looking for a way to change views based on props but couldn't find an existing pattern, most of them do it in JS like multiline/singleline text input)

Do we correctly handle adding and removing blur filter? I can see logic there

Yes, it mimics the existing container approach, so cleans it up when prop is removed.

Is there any impact to things like hit testing within the filtered view?

Yeah, there is. It should be fixed once we merge this - #52413. Will add example in hit testing when blur/SwiftUI view is on.

Do we have any examples with filters + transparent borders, which was IIRC the interesting container view case?

The usecase for container view iirc was overflow + boxShadow. i.e. even when overflow is hidden, boxShadow still needs to be out of bounds. This works with the blur since the container becomes child of the SwiftUI content view. Will add example to cover this.

hue-rotate, contrast, brightness, grayscale

Yes! Once we finalize the approach, will do a separate PR to add them and replace the brightness filter and also the feature flag.

@joevilches
Copy link
Contributor

Thanks! This is so so awesome. I spent many days racking my brain trying to get this to work last year. Super excited to see you push through here!

I think the general approach is good, but I am trying to think of ways to reduce complexity, since the containerView I added a while back is quite confusing already, and now we are adding more "container views" for separate purposes. Figuring out where to add layers, subviews, etc is now quite complicated. For instance in the worst case you can have 4 subviews under a RCTViewComponentView before getting to the "actual" subviews - 2 views to support filter, 1 view to support clipping (currentContainerView), 1 view to support special content like on Image (contentView).

Some ideas

  • Removing the filter layer in the future. I see you mention that was planned so yay!
  • I would try to encapsulate the fact that we need 2 UIViews to get this to work, that seems like a design detail. Ideally we remove those getters from the wrapper, and just have the wrapper expose the proper APIs need to insert this "View" somewhere in the hierarchy. I can imagine that would get a bit funky when you need to add stuff to the existing hierarchy, so I do not really think we can remove those APIs entirely, but its food for thought. Like I think anything layout related can be handled by the wrapper, since the hosting view and content view should be the same size. So the ask here would be to try and encapsulate it where it makes sense.
  • Would also love if we did not have to do transferVisualPropertiesFromView, but realize that that is a bit challenging. I think ideally this all just sits on top of RCTViewComponentView when we have a filter, but I do not think we support that very well right now. Something in the mounting layer would have to change. But if we can do that then we remove a ton of complexity. I can ask around and see if this is something we can do.
  • Not asking you to do this, it should be a separate PR, but: we really need some "content manager" or something to handle all the juggling. My brain hurts trying to figure out where everything goes. I think there is a future where everything we add to RCTViewComponentView goes through this manager and it adds the property to the right "content view", so we don't really need to think about if we missed some case by adding some layer/property/subview to the right place. cc @NickGerleman what do you think? I could try to tackle that during week when I am working on better engineering more.

Copy link
Contributor

@joevilches joevilches left a comment

Choose a reason for hiding this comment

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

requesting changes for some of the things Nick asked for (like the feature flag), along with some of my nits, and an attempt to encapsulate some of the 2-view logic that is going on

@@ -63,7 +63,7 @@ void ViewShadowNode::initialize() noexcept {
viewProps.mixBlendMode != BlendMode::Normal ||
viewProps.isolation == Isolation::Isolate ||
HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) ||
!viewProps.accessibilityOrder.empty();
!viewProps.accessibilityOrder.empty() || !viewProps.filter.empty();
Copy link
Contributor

Choose a reason for hiding this comment

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

we have this already on line 62


- (UIView *)contentView;
- (void)updateBlurRadius:(NSNumber *)radius;
- (void)updateGrayScale:(NSNumber *)amount;
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't seem like we use this. Can we either get rid of it here and add it later in another PR, or change RCTViewComponentView code to use?

@@ -793,44 +802,88 @@ - (BOOL)styleWouldClipOverflowInk
((!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth)) || _props->outlineWidth != 0);
}

- (UIView *)childContainerView
Copy link
Contributor

Choose a reason for hiding this comment

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

name is kinda confusing, so is mine below. Can you be specific? This is only used for filter maybe change it to filterView or something

Comment on lines 812 to 814
_swiftUIWrapper.hostingView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
UIView *swiftUIContentView =
[[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like the wrapper can handle this and we can encapsulate these 2 calls inside and just call some function the wrapper exposes that sets the frame/inits the content view

Comment on lines 581 to 586
if (_swiftUIWrapper) {
_swiftUIWrapper.hostingView.frame = self.bounds;
if (_swiftUIWrapper.contentView) {
_swiftUIWrapper.contentView.frame = self.bounds;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we encapsulate layout updates into the wrapper?

@joevilches
Copy link
Contributor

oh sorry one more thing - can we test this with some combo of clipping and overflow ink (box shadow, outline, etc).

As an aside: we should add an "amalgamation" example in RNTester that just adds every style to one unfortunate View. A lot of these changes only break when we combine it with all the other fanciness we have added.

@realsoelynn
Copy link
Contributor

Can we add an example for applying blurRadius with animation as well?

cc: @joevilches

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 25, 2025

Thank you @joevilches 🙏. Will make the above mentioned changes and update the PR.

But if we can do that then we remove a ton of complexity. I can ask around and see if this is something we can do.

This would be really amazing. We can make RCTViewComponentView simple again 🥲. I was just checking and seems ShadowViewMutation::Create has prop info and if we could pass it to dequeueComponentViewWithComponentHandle and return a new component descriptor based on it that could potentially solve it? We can remove the existing and the new Swift UI container to dedicated RCTViewComponentView inherited views.

@thomazcapra
Copy link

👀

Animated.timing(animatedValue, {
toValue: isBlurred ? 0 : 20,
duration: 1000,
useNativeDriver: false,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For native driver support we need to merge this - #52920

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants