Skip to content

feat: refactor snapshots when going back on Fabric#2134

Merged
WoLewicki merged 26 commits into
mainfrom
@wolewicki/fix-android-fabric-goback
Jul 31, 2024
Merged

feat: refactor snapshots when going back on Fabric#2134
WoLewicki merged 26 commits into
mainfrom
@wolewicki/fix-android-fabric-goback

Conversation

@WoLewicki

@WoLewicki WoLewicki commented May 14, 2024

Copy link
Copy Markdown
Member

Description

PR adding snapshots when going back on Fabric on Android and changing the behavior a bit on iOS.

@WoLewicki WoLewicki marked this pull request as ready for review May 16, 2024 13:21
@WoLewicki WoLewicki changed the title feat: first look at working version on fabric feat: refactor snapshots when going back on Fabric May 16, 2024

@tboba tboba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great work! Overall, this code looks good to me. Just left a few remarks there 🎉

Comment thread RNScreens.podspec
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
platform = new_arch_enabled ? "11.0" : "9.0"
source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/**/*.{cpp,h}"]
source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/RNScreensTurboModule.cpp", "cpp/RNScreensTurboModule.h"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why we're not scoping all .cpp and .h files right now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because the new files added to the folder should not be compiled on old arch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe it's worth to consider grouping the files. E.g. place screen-transition related staff in a cpp/transition / cpp/turbomodule directory & new screen listeners into cpp/mounting or sth similar.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can we do a cleanup of those in the follow-up PR?

Comment thread android/CMakeLists.txt Outdated
Comment thread android/src/main/cpp/NativeProxy.h
@tboba

tboba commented May 20, 2024

Copy link
Copy Markdown
Contributor

Hi @WoLewicki! While I was testing all examples from FabricTestExample, I've stumbled onto one app crash while being on the branch from this PR. When I'm trying to run Test640 and I'm coming back to the previous screen (from the screen with TouchableOpacities), I'm getting crash with exception AssertionException and IndexOutOfBoundsException. Do you know what might be the cause of this? 🤔

Exception from Logcat
2024-05-20 15:56:17.375 23934-23978 unknown:SoftAssertions  com.fabrictestexample                E  Unhandled SoftException
                                                                                                    com.facebook.react.bridge.AssertionException: Expected to run on UI thread!
                                                                                                    	at com.facebook.react.bridge.SoftAssertions.assertCondition(SoftAssertions.java:37)
                                                                                                    	at com.facebook.react.bridge.UiThreadUtil.assertOnUiThread(UiThreadUtil.java:44)
                                                                                                    	at com.facebook.react.fabric.FabricUIManager.resolveView(FabricUIManager.java:921)
                                                                                                    	at com.swmansion.rnscreens.NativeProxy.notifyScreenRemoved(NativeProxy.kt:26)
                                                                                                    	at com.facebook.jni.NativeRunnable.run(Native Method)
                                                                                                    	at android.os.Handler.handleCallback(Handler.java:958)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:99)
                                                                                                    	at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:205)
                                                                                                    	at android.os.Looper.loop(Looper.java:294)
                                                                                                    	at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:233)
                                                                                                    	at java.lang.Thread.run(Thread.java:1012)
2024-05-20 15:56:17.420 23934-23934 AndroidRuntime          com.fabrictestexample                D  Shutting down VM
2024-05-20 15:56:17.422 23934-23934 AndroidRuntime          com.fabrictestexample                E  FATAL EXCEPTION: main
                                                                                                    Process: com.fabrictestexample, PID: 23934
                                                                                                    java.lang.IndexOutOfBoundsException: getChildDrawingOrder() returned invalid index 1 (child count is 1)
                                                                                                    	at android.view.ViewGroup.getAndVerifyPreorderedIndex(ViewGroup.java:2100)
                                                                                                    	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4314)
                                                                                                    	at android.view.View.draw(View.java:23892)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22756)
                                                                                                    	at android.view.View.draw(View.java:23620)
                                                                                                    	at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
                                                                                                    	at com.facebook.react.views.view.ReactViewGroup.drawChild(ReactViewGroup.java:873)
                                                                                                    	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4341)
                                                                                                    	at com.facebook.react.views.view.ReactViewGroup.dispatchDraw(ReactViewGroup.java:846)
                                                                                                    	at android.view.View.draw(View.java:23892)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22756)
                                                                                                    	at android.view.View.draw(View.java:23620)
                                                                                                    	at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
                                                                                                    	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4341)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22747)
                                                                                                    	at android.view.View.draw(View.java:23620)
                                                                                                    	at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
                                                                                                    	at androidx.coordinatorlayout.widget.CoordinatorLayout.drawChild(CoordinatorLayout.java:1312)
                                                                                                    	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22747)
                                                                                                    	at android.view.View.draw(View.java:23620)
                                                                                                    	at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
                                                                                                    	at com.swmansion.rnscreens.ScreenStack.performDraw(ScreenStack.kt:315)
                                                                                                    	at com.swmansion.rnscreens.ScreenStack.access$performDraw(ScreenStack.kt:15)
                                                                                                    	at com.swmansion.rnscreens.ScreenStack$DrawingOp.draw(ScreenStack.kt:327)
                                                                                                    	at com.swmansion.rnscreens.ScreenStack.drawAndRelease(ScreenStack.kt:282)
                                                                                                    	at com.swmansion.rnscreens.ScreenStack.dispatchDraw(ScreenStack.kt:298)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22747)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540)
                                                                                                    	at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513)
                                                                                                    	at android.view.View.updateDisplayListIfDirty(View.java:22712)
                                                                                                    	at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:694)
                                                                                                    	at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:700)
                                                                                                    	at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:798)
                                                                                                    	at android.view.ViewRootImpl.draw(ViewRootImpl.java:4939)
                                                                                                    	at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4643)
                                                                                                    	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3822)
                                                                                                    	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2465)
                                                                                                    	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9305)
2024-05-20 15:56:17.422 23934-23934 AndroidRuntime          com.fabrictestexample                E  	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1339)
                                                                                                    	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1348)
                                                                                                    	at android.view.Choreographer.doCallbacks(Choreographer.java:952)
                                                                                                    	at android.view.Choreographer.doFrame(Choreographer.java:882)
                                                                                                    	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1322)
                                                                                                    	at android.os.Handler.handleCallback(Handler.java:958)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:99)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:205)
                                                                                                    	at android.os.Looper.loop(Looper.java:294)
                                                                                                    	at android.app.ActivityThread.main(ActivityThread.java:8177)
                                                                                                    	at java.lang.reflect.Method.invoke(Native Method)
                                                                                                    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
                                                                                                    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

@tboba tboba self-requested a review June 11, 2024 08:37

@tboba tboba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Besides two comments, everything LGTM!

Comment thread FabricTestExample/ios/Podfile.lock Outdated
PODFILE CHECKSUM: 67b3d295da87c29349179e51bb3526b67059b646

COCOAPODS: 1.15.2
COCOAPODS: 1.14.3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we shouldn't introduce some policy of using chosen version of cocoapods to omit this sort of changes from Podfiles 🤔 what do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, would be nice to just hardcode it so it does not land in many PRs by mistake

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This depends on cocoapods version you have installed locally I believe. I don't think we should include this in your PR, especially that it's a lower version, and there should be no need to downgrade.

Comment thread android/CMakeLists.txt Outdated
Comment on lines +6 to +8
if(${IS_NEW_ARCHITECTURE_ENABLED})
string(APPEND CMAKE_CXX_FLAGS " -DRCT_NEW_ARCH_ENABLED")
endif()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we setting this? Is that because of some unmerged changes earlier for matching RN 0.74?

facebook-github-bot pushed a commit to react/react-native that referenced this pull request Jun 14, 2024
Summary:
PR changing the single mountingOverrideDelegate to a vector of those, so other listeners can operate on the transaction. Used by `react-native-screens` in software-mansion/react-native-screens#2134 and `react-native-reanimated` in software-mansion/react-native-reanimated#6055.

Till now, only one listener could be added there, meaning that e.g. `Layout Animations` from `react-native`,  `Layout Animations` from `react-native-reanimated` and listening for `Screen` removal in `react-native-screens` could not operate at the same time.

## Changelog:

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[GENERAL] [FIXED] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->

[GENERAL] [FIXED] - Add option for multiple `mountingOverrideDelegates`

Pull Request resolved: #44927

Test Plan: The code of `LayoutAnimations` inside `react-native` should work the same since it will add just one listener then. For other cases, different libraries can read/mutate transactions.

Reviewed By: javache

Differential Revision: D58530278

Pulled By: sammy-SC

fbshipit-source-id: d6305963621000be11d51a50cffff64526cca934

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay, I think I have solid grasp of the changes. These looks solid 💪🏻 I still have some questions and change requests I've posted in the review.

Note I think we should vastly improve the PR description, with more information on the solution architecture, especially initialisation phase of the C++ layer and some simple flow of the solution.

Comment thread FabricTestExample/Gemfile.lock Outdated
concurrent-ruby (~> 1.0)
json (2.7.2)
minitest (5.22.3)
minitest (5.22.2)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's going on here, why do we change that?

Comment thread FabricTestExample/ios/Podfile.lock Outdated
PODFILE CHECKSUM: 67b3d295da87c29349179e51bb3526b67059b646

COCOAPODS: 1.15.2
COCOAPODS: 1.14.3

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This depends on cocoapods version you have installed locally I believe. I don't think we should include this in your PR, especially that it's a lower version, and there should be no need to downgrade.

Comment thread RNScreens.podspec
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
platform = new_arch_enabled ? "11.0" : "9.0"
source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/**/*.{cpp,h}"]
source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/RNScreensTurboModule.cpp", "cpp/RNScreensTurboModule.h"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe it's worth to consider grouping the files. E.g. place screen-transition related staff in a cpp/transition / cpp/turbomodule directory & new screen listeners into cpp/mounting or sth similar.

Comment thread android/CMakeLists.txt
Comment on lines -15 to -19
set_target_properties(rnscreens PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
POSITION_INDEPENDENT_CODE ON

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we replace setting properties on particular target with setting the non-target-scoped variable CMAKE_CXX_STANDARD? Also why do we drop standard requirement, turning off extensions and enabling -fpic explicitly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure why I removed it, reverted.

Comment thread android/CMakeLists.txt
target_compile_definitions(
rnscreens
PRIVATE
-DFOLLY_NO_CONFIG=1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
-DFOLLY_NO_CONFIG=1
FOLLY_NO_CONFIG=1

I believe you can omit -D prefix in target_compile_definition (link)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It doesn't work if I remove it so I think it is not true.

@kkafar kkafar Jul 17, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not gonna lie, I'm surprised as CMake docs I've linked explicitly state that you can omit -D, but that's a nitpick anyway, we can proceed w/o this change.

Comment thread cpp/RNSScreenRemovalListener.h Outdated
struct RNSScreenRemovalListener : public MountingOverrideDelegate {
std::function<void(int)> listenerFunction_;
RNSScreenRemovalListener(std::function<void(int)> &&listenerFunction_)
: listenerFunction_(listenerFunction_) {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

haven't opened the code, writing from memory: don't we need here to have std::move / std::forward called to invoke move constructor of std::function?

Suggested change
: listenerFunction_(listenerFunction_) {}
: listenerFunction_(std::move(listenerFunction_)) {}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It works so I guess we don't?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It is not matter of whether it works, it is matter of how it works.

See this snippet for the explanation.

Heres the example code in case the snippet times out (you can paste it to an online C++ compiler, e.g. this one

Code snippet

#include <iostream>

class SomeClass {
    int someField;

public:
    // DefaultConstructor
    SomeClass() : someField{0} {
        std::cout << "DefaultConstructor invoked\n";
    }

    // CopyConstructor
    SomeClass(const SomeClass& other) {
        std::cout << "CopyConstructor invoked\n";
        this->someField = other.someField;
    }

    // MoveConstructor
    SomeClass(SomeClass&& other) {
        std::cout << "MoveConstructor invoked\n";
        this->someField = std::move(other.someField);
    }
};

class CompositeClass {
    SomeClass someClassInstance;
public:
    CompositeClass(SomeClass&& other) : someClassInstance(other) {}

    // LOOK HERE!!!!
    // Comment the line above and uncomment the line below to see the difference.
    // CompositeClass(SomeClass&& other) : someClassInstance(std::move(other)) {}
};

// Type your code here, or load an example.
int main() {
    SomeClass someClassInstance{};
    CompositeClass ccInstance(std::move(someClassInstance));
    return 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And now, after testing I'm sure we shall have std::move there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can you explain what is the difference exactly and how it affects the code I added?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I provided a code snippet with mimicking the setup you have here in this PR & appended comments explaining.
The difference comes down to which constructor is being called & that's why I've put "couts" in ctors of SomeClass class.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Basically right now you take rvalue reference as RNSScreenRemovalListener constructor parameter just to later pass it by value to initialiser of listenerFunction_ field effectively copying it (invoking copy constructor). By moving there, as I've suggested, you invoke move constructor of std::function forwarding the parameter value correctly w/o unnecessary copy.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done

ShadowViewMutationList mutations) const {
for (ShadowViewMutation mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Remove &&
mutation.oldChildShadowView.componentName != nullptr &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we check oldChildShadowView.componentName for nullish value while later using parentShadowView.componentName for comparison purposes?

Also, are we looking for RNSScreenStack here, cause we want this fix to work only for native-stack? Isn't this (whiteflash) also an issue with ScreenContainer? (I don't have knowledge here, just wondering reading through the code).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why do we check oldChildShadowView.componentName for nullish value while later using parentShadowView.componentName for comparison purposes?

Because we use oldChildShadowView.componentName in listenerFunction_.

Also, are we looking for RNSScreenStack here, cause we want this fix to work only for native-stack? Isn't this (whiteflash) also an issue with ScreenContainer? (I don't have knowledge here, just wondering reading through the code).

Indeed it is only needed in RNSScreenStack since other navigators do not remove the view until the animation is completed.

Comment thread ios/RNSModule.mm Outdated
Comment thread ios/RNSModule.mm Outdated
Comment thread ios/RNSModule.mm Outdated
@chrispader chrispader mentioned this pull request Sep 2, 2024
8 tasks
ja1ns pushed a commit to WiseOwlTech/react-native-screens that referenced this pull request Oct 9, 2024
…oftware-mansion#2261)

## Description

> [!note] 
This PR applies to iOS only. 

Ok, so this PR is related to software-mansion#2247 & to get broader context I highly
recommend to read [this
comment](software-mansion#2247 (review))
at the very minimum.

### Issue context

On Fabric during JS initialised Screen dismissal (view removing in
general) children are unmounted before their parents, thus when
dismissing screen from screen stack & starting a dismiss transition all
Screen content is already removed & we're animating only a blank screen
resulting in visual glitch.

### Current approaches

Right now we're utilising `RCTMountingTransactionObserving` protocol,
filter all mounting operations *before* they are applied and if screen
dismissal is to be done, we take a snapshot of to-be-removed-screen.

### Alternative approaches

software-mansion#2134 sets mounting coordinator delegate and effectively does the same
as the current approach, however it can also be applied to Android.

### Proposed approach

On iOS we can utilise the platform & how it works. Namely the fact of
unmounting child view does not impact the hardware buffer, nor bitmap
layer immediately, thus we can take the snapshot simply in `- [RNSScreen
unmountChildComponentView: index:]` and the children will still be
visible.

This approach is safe and reliable, because:

##### 10k feet explanation

Drawing is not performed immediately after an update to UIKit model
(such as removing a view), the system handles all operations pending on
main queue and just after that it schedules drawing. We're removing the
views & making snapshot in the middle of block execution on the main
thread, thus the drawing can't happen and just-unmounted-views will be
visible on the snapshot.

##### More detailed explanation

1. the main thread run loop of Cocoa application drains the main queue
till it's empty
[[1]](https://opensource.apple.com/source/CF/CF-1153.18/CFRunLoop.c.auto.html)
[[2]](https://fabernovel.github.io/2021-01-04/uikit-rendering-part-3)
[[5]](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1)
2. CoreAnimation framework integrates with the main run loop by
registering an observer and listening for `kCFRunLoopBeforeWaiting`
event (so after the main queue is drained & run loop is to become idle
due to no more pending tasks).
[[2]](https://fabernovel.github.io/2021-01-04/uikit-rendering-part-3)
3. CoreAnimation is responsible for applying all transactions from the
last loop pass & sending them to render server (this happens on main
thread), which in turn finally leads up to the changes being applied,
drawn & displayed (this happens on different threads).
[[2]](https://fabernovel.github.io/2021-01-04/uikit-rendering-part-3)
[[3]](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/WindowsandViews/WindowsandViews.html#//apple_ref/doc/uid/TP40009503-CH2-SW1)
4. [We know that the RN's mounting stage will be executed on main
thread](https://github.com/facebook/react-native/blob/91ecd7eb5330f5d725f5587744713064d614a6b3/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm#L258),
because UIKit is thread-safe only in selected parts and requires calling
from the main thread.
5. Single RN transaction is a complete diff between ["rendered tree" &
"next
tree"](https://reactnative.dev/architecture/render-pipeline#phase-2-commit-1)
and is performed [atomically &
synchronously](https://github.com/facebook/react-native/blob/91ecd7eb5330f5d725f5587744713064d614a6b3/packages/react-native/ReactCommon/react/renderer/mounting/TelemetryController.cpp#L18-L51)
on [main
thread](https://github.com/facebook/react-native/blob/91ecd7eb5330f5d725f5587744713064d614a6b3/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm#L258),
thus whole batch of updates will be finished before drawing instructions
will be send to render server.

#### Reference:


[[1]](https://opensource.apple.com/source/CF/CF-1153.18/CFRunLoop.c.auto.html)
(Look for `__CFRunLoopDoBlocks(...)` & `__CFRunLoopRun(...)` functions)

Important thing to notice if `__CFRunLoopDoBlocks` is that it locks the
`rl` (run loop) lock, takes & copies reference to the list of the blocks
to execute, clears the original list of blocks and releases the `rl`
lock. Thus only the "already scheduled" blocks are executed in the
single pass of this function. It is called multiple times in the single
pass of the run loop, but I haven't dug deeper, it should be enough for
our use case that we have guarantee that all the blocks are drained.

[[2]](https://fabernovel.github.io/2021-01-04/uikit-rendering-part-3)
(Blog post on rendering in UIKit)


[[3]](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/WindowsandViews/WindowsandViews.html#//apple_ref/doc/uid/TP40009503-CH2-SW1)
(Apple docs - The View Drawing Cycle section)

[[4]](https://bou.io/RunRunLoopRun.html) (Blog post on the run loop)


[[5]](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1)
(Apple docs on run loops)

## Changes

* Snapshot is not done in `unmountChildComponentView: index:` & only
when needed.
* Removed old mechanism
* Removed now unused implementation of `RCTMountingObserving` protocol

## Test code and steps to reproduce

Run any example on Fabric, push a screen, initiate go-back via JS (e.g.
by clicking a button with `navigation.goBack()` action), see that the
screen transitions correctly (the content is visible throughout
transition)

## Checklist

- [x] Ensured that CI passes
ja1ns pushed a commit to WiseOwlTech/react-native-screens that referenced this pull request Oct 9, 2024
…2134)

PR adding snapshots when going back on Fabric on Android and changing the behavior a bit on iOS.
alduzy added a commit that referenced this pull request Oct 30, 2024
## Description

This PR intents to fix current screen going blank on Android when
returning to the previous one. The issue has been already fixed by
#2134, but
re-appeared recently. After a quick bisect it turned out that
#2412
caused the regression. Bringing back the removed `NativeScreensModule`
export fixes the issue.

Fixes #1685 

## Changes

- added missing export
- modified `Test2282.tsx` repro

## Screenshots / GIFs

### Before


https://github.com/user-attachments/assets/39bf6c66-39d2-4dfd-abc8-ee1c690417a4

### After


https://github.com/user-attachments/assets/003446b2-c976-4cdb-8ab4-348dd619ba79

## Test code and steps to reproduce

- use `Test2282.tsx` repro

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
WoLewicki added a commit that referenced this pull request Nov 14, 2024
## Description

Based on
Expensify/App#49937 (comment) and
the comments above, PR fixes the unnecessary checks for creating
snapshot on the view on dismiss. It was refactored already in
#2134 and
#2261 (👏 to
@kkafar). Those checks do nothing now since each screen is responsible
for making its own snapshot. Having those checks can only lead to
problems.
jamesdev-king added a commit to jamesdev-king/flutter_ui_challenge_flight_search that referenced this pull request Aug 1, 2025
## Description

This PR intents to fix current screen going blank on Android when
returning to the previous one. The issue has been already fixed by
software-mansion/react-native-screens#2134, but
re-appeared recently. After a quick bisect it turned out that
software-mansion/react-native-screens#2412
caused the regression. Bringing back the removed `NativeScreensModule`
export fixes the issue.

Fixes #1685 

## Changes

- added missing export
- modified `Test2282.tsx` repro

## Screenshots / GIFs

### Before


https://github.com/user-attachments/assets/39bf6c66-39d2-4dfd-abc8-ee1c690417a4

### After


https://github.com/user-attachments/assets/003446b2-c976-4cdb-8ab4-348dd619ba79

## Test code and steps to reproduce

- use `Test2282.tsx` repro

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
jamesdev-king added a commit to jamesdev-king/flutter_ui_challenge_flight_search that referenced this pull request Aug 1, 2025
## Description

Based on
Expensify/App#49937 (comment) and
the comments above, PR fixes the unnecessary checks for creating
snapshot on the view on dismiss. It was refactored already in
software-mansion/react-native-screens#2134 and
software-mansion/react-native-screens#2261 (👏 to
@kkafar). Those checks do nothing now since each screen is responsible
for making its own snapshot. Having those checks can only lead to
problems.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants