Compare commits

..

36 Commits

Author SHA1 Message Date
Krzysztof Magiera
6f8b460549 Bump version -> 2.0.0-beta.13 2020-02-24 23:31:16 +01:00
Krzysztof Magiera
b52aea3fd2 Hotfix for the case when views are invalidated but not removed from subviews
The above situation may happen when grandparent is unmounted from react. In such a case children are not removed from non direct ancestors but still get invalidated.
2020-02-24 23:30:02 +01:00
Krzysztof Magiera
4e72ad050b Bump version -> 2.0.0-beta.12 2020-02-24 22:26:32 +01:00
Krzysztof Magiera
0dd03e2c53 Revert container frame update change from #354 (#379)
Reverting that change as it turned out not to be necessary. At the moment of attaching screen we already have the frame set on the child VC. Updating it there again was cauinsg some visual glitches with old react-navigation screen container based implementation.
2020-02-24 22:26:01 +01:00
Krzysztof Magiera
e38c5bfdc2 Fix memory leak related to access of presentationController. (#378)
After exaiming memory graph it turned out that screens for which we access presentationController property ends up leaking, likely because of some issue in UIKit. This turns out not to affect screens that are actually being presented as modals (when presentationController is required). This change updates presentation mode setter to avoid accessing presentationController for push screens and also warns if the prop ever switches from modal to push (in which case we cannot reverse the leak source).
2020-02-24 22:14:34 +01:00
Krzysztof Magiera
cdbf4e463f Bump version -> 2.0.0-beta.11 2020-02-24 18:06:09 +01:00
Krzysztof Magiera
4e8d13dc72 Verify integrity of fragment manager before executePendingTransa… (#377) 2020-02-24 18:03:46 +01:00
Krzysztof Magiera
d3b6bea594 Fix no view found crash in fragment manager. (#376)
This change fixes issue reported in #54. The issue was caused by the fragment transaction that run past the moment when container view is detached. This could happen when container is quickly added and removed as fragment transactions sometimes may take long time to execute. Unfortunately enqueued fragment transactions cannot be cancelled, so to make sure that transaction isn't run past container unmount we call executePendingTransactions right before the screen is detached.
2020-02-24 17:42:44 +01:00
Krzysztof Magiera
b29e634e26 Fix appear/disappear events for ScreenContainer and address memo… (#354)
This chnage fixes the way we'd managed parent<>child VC relation. With this change in we hook child VC to parent in didMoveToWindow to match Stack behavior. We also wait with updating child view frame untill the child screen is attached. Finally we utilize RCTInvalidating interface to spot moments when screen controller is unmounted from react such that we can break reference cycle between screen and screen view (we don't do it in invalidate directly as sometimes we need to wait till transition end).
2020-02-21 22:40:44 +01:00
Krzysztof Magiera
f2caf02d8c Fix didMoveToWindow logic. (#352)
This change fixes logic that we run when stack is moved to a window. Before we'd first attach stack to parent VC and then run updateContainer. This could've led to a buggy behavior in case the stack is mounted within other stack that is running a transition (e.g. dismissing a modal). In such a case setting initial push VCs would fail (because UIKit does not allow push to be modified while transitioning). Because of that stack would initialize with the dummy VC and the initial VC would get added to dismissedScreens (which is another side effect of the logic that keeps track of dismissed screens).

The fix was to add call to updateContainer before VC is added to parent. This makes it possible for push screens to be properly initialized (since VC is not yet added it does not know its parent is transitioning). Then, since updating modals handles gracefully the case when modals cannot be shown (and they wont be shown unless added to parent VC) we can safely run updateContainer yet another time after the VC is added to parent.

Howeer after the above fix was applied we observed another issue that was due to an invalida appear/disappear event management within view controllers. To address that fix we no longer allow navigaton VC's view to be added to the container view unless it starts transition to parent view controller. We also fixed missing calls to super in willMoveToParentViewController callback of RNSScreen controller and also fixed view reference management withing RNScreen to keep views as weak references while the view is not attached. The latter fixes the problem with screen view leaking under certain conditions.
2020-02-21 18:06:38 +01:00
Krzysztof Magiera
c8845cbb6a Bump version -> 2.0.0-beta.10 2020-02-21 00:17:28 +01:00
Krzysztof Magiera
9bf2edd405 Revert updateContainer threading changes from #351. 2020-02-21 00:16:43 +01:00
Krzysztof Magiera
748cdc6fba Bump version -> 2.0.0-beta.9 2020-02-20 18:05:28 +01:00
Krzysztof Magiera
89cf0b7e52 Fix detaching VC from parent VC. (#351)
This change fixes the problem of container view controllers not being detached properly from its parent container VC. We fix this by detaching VC from didMoveToWindow. On top of that we also added a check to ensure the update of containers won't run unless JS batch is complete. This is to prevent partially updated headers.
2020-02-20 17:50:20 +01:00
Janic Duplessis
db4733ad05 Fix gestureEnabled for modal with no header on iOS (#325)
Since the config is only updated in the HeaderConfig view it will not get executed for stacks with no header. This fixes it by setting the prop directly in the Screen when the gestureEnabled prop is set.
2020-02-19 20:33:58 +01:00
Radek Czemerys
8952e698d2 docs: Replace useScreens with enableScreens for expo docs (#348)
fixes #346
2020-02-19 18:09:25 +01:00
Krzysztof Magiera
953b3b30db Bump version -> 2.0.0-beta.8 2020-02-19 15:37:10 +01:00
Grzegorz Mandziak
e50b7eae72 check object instance of before calling detachScreen (#344)
Fixes #343 

This change filters out non ScreenFragment instances when iterating through the list of installed fragments. The reason why other fragments can be added to the manager is that some libraries (e.g. glide) use fragment manager to store screen local data and attach to the screen lifecycle.
2020-02-19 15:30:58 +01:00
Krzysztof Magiera
0e00f49e69 Fix updating push native stack to avoid mid-transition related b… (#347)
This change addresses two problem. First was related to header subviews that were not laid out properly before we installed them in the header. This was causing occasional bugs where the header subviews were misplaced. The fix was to enforce container update run after layout is done (we enqueue update on ui thread directly from didUpdateReactSubviews).

The second problem was related to UINavController nlifecycle methods not triggering correctly in case when updates are being made to the nav controller which isn't mounted. Previously we fixed similar issue for modal controllers where because of the fact container wasn't mounted we couldn't run modal screen updates. With push controllers the problem is very similar however the VC do update just stop getting proper lifecycle updates and warning "Unbalanced calls to begin/end appearance transitions" is displayed. The fix was similar as in the modal case, that is, we wait until container is installed in window.
2020-02-19 15:22:55 +01:00
Krzysztof Magiera
26d8dc21d2 Bump version -> 2.0.0-beta.7 2020-02-18 16:49:11 +01:00
Krzysztof Magiera
3193e7da8f Update iOS native modals presenting logic to be more resilient (#342)
This update changes the logic of how we present modals. Previously presentedModals array could get out of sync with the actual app state because in case when parent VC is not mounted, presenting a modal would silently fail. In order to handle that we added additional update trigger from where the screen stack is mounted to a window. Second change that we added here addresses a problem of concurrent calls to update modals. As we cannot update presentedModals array synchrounously (all presentation callbacks are async even when no animation is involved), we need to block future updates unless the previous batch of updates is done. To do that we added a flag that prevents method re-entry. The flag is reset when all the presentation animations are over.
2020-02-18 16:48:43 +01:00
Krzysztof Magiera
cce8373a20 Android fix for invisible stack views after navigating back (#341)
After fragment library upgrade we observed a regression caused by the screens that we navigate back to being invisible. This turned out to be a problem with view restore mechanism that we don't rely on. On native android the detached view state is dumped and then view's visibility is change to GONE after screen animated away. However, since we don't rely on view state restore and instead just reuse the whole view object, when navigating back we'd move to a view with visibility set to GONE. This change workarounds this problem in the method responsible for recycling views where we reset visibility flag back to VISIBLE.
2020-02-18 15:40:56 +01:00
Matt Oakes
823d11e691 feat: Add in additional modal types for iOS (#318)
Allows you to choose additional modal presentation styles for iOS. It adds the ability to force a full screen modal or choose a "form sheet" style. This only affects iOS.
2020-02-17 21:12:57 +01:00
Krzysztof Magiera
1aeba7faa1 Bump version -> 2.0.0-beta.6 2020-02-17 17:09:13 +01:00
Krzysztof Magiera
e6ed4176cd Fix condition that detected whether we should update screen fram… (#336)
The previous condition was broken as under certain circumstances we would've receive a setReactFrame with no active screen rendered to only get the screen activate in a while. This resulted in the view dimention not being properly updated. This diff changes the condition and verifies whether a screen is mounted under UINavController or not. When not, we assume it is mounted under regular screen container and allow the frame to be adjusted from react.
2020-02-17 17:08:52 +01:00
Krzysztof Magiera
4f792b4281 Bump version -> 2.0.0-beta.5 2020-02-17 16:41:51 +01:00
Krzysztof Magiera
d35c523c37 Default to absolute fill for stack screen items. (#335)
This change makes navigation screen wrapper to set absolute fill style for screen items wrendered within native stack. We obvserved an issue where screens are rendered with a versy small height when the initial style is not set properly on iOS. This change makes the screen default to full screen and only then be resized down in case navigation bars are set.
2020-02-17 16:41:22 +01:00
Krzysztof Magiera
67806cbbb5 Fix several crashes in native stack related to fragment library (#331)
This change fixes two crashes related to fragment library. The first issue was a crash caused by return transaction started while the previous transaction was ongoing (e.g., quickly adding new screen on top of the stack and immediately dismissing it). The main fix was applied in the fragment library and therefore as a part of this change we update the dependency to fragment:1.2.1 which is the current latest stable version. As a result of the fragment library change we started observing other issue. The second issue was caused by the fact that under certain circumstances the view associated with a fragment couldn't been added despite it still being attached to a parent. This was resulting in a crash. This change adds a cleanup code that properly detaches the view: we do it in onCreateView but also when the fragment destroys its view (in onViewDestroy callback). The latter is necessary because when fragments are restored the order of onCreateView calls is reversed which causes inner views to attach despite their fragment managers not being initialized.
2020-02-15 01:12:59 +01:00
Krzysztof Magiera
6212847218 Bump version -> 2.0.0-beta.4 2020-02-14 17:03:20 +01:00
Krzysztof Magiera
a87faf443f Remove unnecessary logging. 2020-02-14 17:03:00 +01:00
Krzysztof Magiera
bdf610bcad Bump version -> 2.0.0-beta.3 2020-02-14 15:13:53 +01:00
Krzysztof Magiera
102880c18b Trigger transition finish event from native stack (#330)
This change adds new event that gets dispatched from native stack when screen transitioning is finished.

The new event is used by react-navigation to update the library internal state that the triggered action has been finished. Previously we were relying solely on onAppear and onDisappear events, however those does not get triggered when new iOS 13 modals are used or when transparent modals are displayed. This is because with transparent modals the view below is still visible. We therefore needed another way of notifying the library that screen transition have finished despite the fact that disappear event couldn't be triggered.

As a part of this change I also refactored invalid ref cycle-break code on iOS which was ought to remove reference cycle between view and view controller. This code have been moved to viewWillDisappear callback.

Also on Android part small refactoring has been done and we removed the necessity to keep mActiveScreens array which was occasionally getting out of sync with the list of active fragments.
2020-02-14 15:13:00 +01:00
Matt Oakes
24b70abd64 fix(android): NullPointerException crash due to null top screen (#323)
The library would crash when the `mTopScreen` instance variable is null. This change checkes if it null before calling the `getScreen()` method. It also marks the method as having a nullable return value. The only place this method is used is already setup to handle `null` responses.
2020-02-11 23:35:10 +01:00
Krzysztof Magiera
f7b6c22591 Force reload back indicator image view when image is nil. (#324)
This fixes a problem when image attribute of image view would get reset and we wouldn't be able to access it even in release mode. This could happen when the app is backgrounded as at that point image views release their resources. Another case that was quite frequent was that the image would start loading before the image view frame was properly set. The solution is to trigger image reload in cases when image is missing and to force the image view frame to match the size of an image.

This change also fixes a problem on iOS pre 13 where we were updating back indicator inside of the method which would run during the transition. Turns out updating this property isn't supported during animation and so we moved it out such that it is updated before the transition starts.
2020-02-11 23:33:41 +01:00
Krzysztof Magiera
ed997ef4ec Skip saving/restoring state for unmounted fragments. (#313)
We currently cannot take any adventage of saving and restoring fragment state because view hierarchy of the unmounted fragment is retained by RN core anyways. Skipping sabe/restore will allow us to avoid unnecessary serializiation/deserialization of the view hierarchy but will also help circumvent some bugs which comes from the side effects of restoring. One of the bugs have been reported in #162 and in that particular case restoring result in InputText change event being triggered with stale value. This was happening in the situation when we were going back to a screen with InputText component.
2020-02-06 20:17:02 +01:00
Krzysztof Magiera
10a0badee2 Fix text selection inside screen container on Android. (#310)
This change fixes the problem with missing text selection modals when text input component is rendered under screen container. The reason turned out to be a problem with Android TextView implementation which expect the component to be attached to window at the moment when layout happens (see Editor.java#prepareCursorControllers and TextView.java$makeNewLayout). Apparently, within screens container layout will happen before the component of a given screen are attached (because attaching is managed by the fragment manager which delays the moment at which the screen is attached). As a result the textView component does not get a chance to fetch proper window layout params such that it can tell whether it should support selection mode or not. We workaround this issue by providing window layout params directly from Screen class which is used instead of the DecorWindow in case TextView is not yet attached (it searches for the top-most parent when not attached).
2020-02-05 23:18:30 +01:00
19 changed files with 504 additions and 152 deletions

View File

@@ -67,9 +67,9 @@ yarn add react-native-screens
2. Open your App.js file and add the following snippet somewhere near the top of the file (e.g. right after import statements):
```js
import { useScreens } from 'react-native-screens';
import { enableScreens } from 'react-native-screens';
useScreens();
enableScreens();
```
3. That's all 🎉  enjoy faster navigation in your Expo app. Keep in mind screens are in pretty early phase so please report if you discover some issues.

View File

@@ -49,8 +49,9 @@ repositories {
dependencies {
implementation 'com.facebook.react:react-native:+'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.fragment:fragment:1.2.1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
}
def configureReactNativePom(def pom) {

View File

@@ -2,18 +2,18 @@ package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.Paint;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.UIManagerModule;
public class Screen extends ViewGroup {
@@ -46,7 +46,7 @@ public class Screen extends ViewGroup {
}
};
private @Nullable Fragment mFragment;
private @Nullable ScreenFragment mFragment;
private @Nullable ScreenContainer mContainer;
private boolean mActive;
private boolean mTransitioning;
@@ -54,8 +54,39 @@ public class Screen extends ViewGroup {
private StackAnimation mStackAnimation = StackAnimation.DEFAULT;
private boolean mGestureEnabled = true;
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
if (mFragment != null) {
mFragment.onViewAnimationEnd();
}
}
public Screen(ReactContext context) {
super(context);
// we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
// not displaying modal menus (e.g., copy/paste or selection). The missing menus are due to the
// fact that TextView implementation is expected to be attached to window when layout happens.
// Then, at the moment of layout it checks whether window type is in a reasonable range to tell
// whether it should enable selection controlls (see Editor.java#prepareCursorControllers).
// With screens, however, the text input component can be laid out before it is attached, in that
// case TextView tries to get window type property from the oldest existing parent, which in this
// case is a Screen class, as it is the root of the screen that is about to be attached. Setting
// params this way is not the most elegant way to solve this problem but workarounds it for the
// time being
setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION));
}
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
// do nothing, react native will keep the view hierarchy so no need to serialize/deserialize
// view's states. The side effect of restoring is that TextInput components would trigger set-text
// events which may confuse text input handling.
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
// ignore restoring instance state too as we are not saving anything anyways.
}
@Override
@@ -144,11 +175,11 @@ public class Screen extends ViewGroup {
mContainer = container;
}
protected void setFragment(Fragment fragment) {
protected void setFragment(ScreenFragment fragment) {
mFragment = fragment;
}
protected @Nullable Fragment getFragment() {
protected @Nullable ScreenFragment getFragment() {
return mFragment;
}

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -22,15 +23,12 @@ import java.util.Set;
public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected final ArrayList<T> mScreenFragments = new ArrayList<>();
private final Set<ScreenFragment> mActiveScreenFragments = new HashSet<>();
private final ArrayList<Runnable> mAfterTransitionRunnables = new ArrayList<>(1);
protected @Nullable FragmentManager mFragmentManager;
private @Nullable FragmentTransaction mCurrentTransaction;
private @Nullable FragmentTransaction mProcessingTransaction;
private boolean mNeedUpdate;
private boolean mIsAttached;
private boolean mIsTransitioning;
private boolean mLayoutEnqueued = false;
@@ -118,36 +116,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
markUpdated();
}
@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mIsTransitioning = true;
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mIsTransitioning) {
mIsTransitioning = false;
notifyTransitionFinished();
}
}
public boolean isTransitioning() {
return mIsTransitioning || mProcessingTransaction != null;
}
public void postAfterTransition(Runnable runnable) {
mAfterTransitionRunnables.add(runnable);
}
protected void notifyTransitionFinished() {
for (int i = 0, size = mAfterTransitionRunnables.size(); i < size; i++) {
mAfterTransitionRunnables.get(i).run();
}
mAfterTransitionRunnables.clear();
}
protected int getScreenCount() {
return mScreenFragments.size();
}
@@ -219,7 +187,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
private void attachScreen(ScreenFragment screenFragment) {
getOrCreateTransaction().add(getId(), screenFragment);
mActiveScreenFragments.add(screenFragment);
}
private void moveToFront(ScreenFragment screenFragment) {
@@ -230,7 +197,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
private void detachScreen(ScreenFragment screenFragment) {
getOrCreateTransaction().remove(screenFragment);
mActiveScreenFragments.remove(screenFragment);
}
protected boolean isScreenActive(ScreenFragment screenFragment) {
@@ -252,16 +218,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
@Override
protected void onDetachedFromWindow() {
// if there are pending transactions and this view is about to get detached we need to perform
// them here as otherwise fragment manager will crash because it won't be able to find container
// view.
if (mFragmentManager != null && !mFragmentManager.isDestroyed()) {
mFragmentManager.executePendingTransactions();
}
super.onDetachedFromWindow();
mIsAttached = false;
// fragment manager is destroyed so we can't do anything with it anymore
mFragmentManager = null;
// so we don't add the same screen twice after re-attach
removeAllViews();
mActiveScreenFragments.clear();
// after re-attach we'll update the screen and add views again
markUpdated();
}
@Override
@@ -282,11 +246,11 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected void onUpdate() {
// detach screens that are no longer active
Set<ScreenFragment> orphaned = new HashSet<>(mActiveScreenFragments);
Set<Fragment> orphaned = new HashSet<>(mFragmentManager.getFragments());
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
ScreenFragment screenFragment = mScreenFragments.get(i);
boolean isActive = isScreenActive(screenFragment);
if (!isActive && mActiveScreenFragments.contains(screenFragment)) {
if (!isActive && screenFragment.isAdded()) {
detachScreen(screenFragment);
}
orphaned.remove(screenFragment);
@@ -294,7 +258,9 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
if (!orphaned.isEmpty()) {
Object[] orphanedAry = orphaned.toArray();
for (int i = 0; i < orphanedAry.length; i++) {
detachScreen((ScreenFragment) orphanedAry[i]);
if (orphanedAry[i] instanceof ScreenFragment) {
detachScreen((ScreenFragment) orphanedAry[i]);
}
}
}
@@ -312,7 +278,7 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
ScreenFragment screenFragment = mScreenFragments.get(i);
boolean isActive = isScreenActive(screenFragment);
if (isActive && !mActiveScreenFragments.contains(screenFragment)) {
if (isActive && !screenFragment.isAdded()) {
addedBefore = true;
attachScreen(screenFragment);
} else if (isActive && addedBefore) {

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -14,6 +15,21 @@ import com.facebook.react.uimanager.UIManagerModule;
public class ScreenFragment extends Fragment {
protected static View recycleView(View view) {
// screen fragments reuse view instances instead of creating new ones. In order to reuse a given
// view it needs to be detached from the view hierarchy to allow the fragment to attach it back.
ViewParent parent = view.getParent();
if (parent != null) {
((ViewGroup) parent).endViewTransition(view);
((ViewGroup) parent).removeView(view);
}
// view detached from fragment manager get their visibility changed to GONE after their state is
// dumped. Since we don't restore the state but want to reuse the view we need to change visibility
// back to VISIBLE in order for the fragment manager to animate in the view.
view.setVisibility(View.VISIBLE);
return view;
}
protected Screen mScreenView;
public ScreenFragment() {
@@ -30,7 +46,7 @@ public class ScreenFragment extends Fragment {
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return mScreenView;
return recycleView(mScreenView);
}
public Screen getScreen() {
@@ -45,19 +61,21 @@ public class ScreenFragment extends Fragment {
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ScreenContainer container = mScreenView.getContainer();
if (container.isTransitioning()) {
container.postAfterTransition(new Runnable() {
@Override
public void run() {
dispatchOnAppear();
}
});
} else {
dispatchOnAppear();
}
public void onResume() {
super.onResume();
}
public void onViewAnimationEnd() {
// onViewAnimationEnd is triggered from View#onAnimationEnd method of the fragment's root view.
// We override Screen#onAnimationEnd and an appropriate method of the StackFragment's root view
// in order to achieve this.
dispatchOnAppear();
}
@Override
public void onDestroyView() {
super.onDestroyView();
recycleView(getView());
}
@Override

View File

@@ -1,11 +1,15 @@
package com.swmansion.rnscreens;
import android.content.Context;
import android.view.View;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@@ -18,6 +22,7 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
private final Set<ScreenStackFragment> mDismissed = new HashSet<>();
private ScreenStackFragment mTopScreen = null;
private boolean mRemovalTransitionStarted = false;
private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
@Override
@@ -50,7 +55,7 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
public Screen getTopScreen() {
return mTopScreen.getScreen();
return mTopScreen != null ? mTopScreen.getScreen() : null;
}
public Screen getRootScreen() {
@@ -90,6 +95,34 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
}
@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mRemovalTransitionStarted = true;
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mRemovalTransitionStarted) {
mRemovalTransitionStarted = false;
dispatchOnFinishTransitioning();
}
}
public void onViewAppearTransitionEnd() {
if (!mRemovalTransitionStarted) {
dispatchOnFinishTransitioning();
}
}
private void dispatchOnFinishTransitioning() {
((ReactContext) getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new StackFinishTransitioningEvent(getId()));
}
@Override
protected void removeScreenAt(int index) {
Screen toBeRemoved = getScreenAt(index);

View File

@@ -1,13 +1,17 @@
package com.swmansion.rnscreens;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -18,6 +22,22 @@ import com.google.android.material.appbar.AppBarLayout;
public class ScreenStackFragment extends ScreenFragment {
private static class NotifyingCoordinatorLayout extends CoordinatorLayout {
private final ScreenFragment mFragment;
public NotifyingCoordinatorLayout(@NonNull Context context, ScreenFragment fragment) {
super(context);
mFragment = fragment;
}
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
mFragment.onViewAnimationEnd();
}
}
private static final float TOOLBAR_ELEVATION = PixelUtil.toPixelFromDIP(4);
private AppBarLayout mAppBarLayout;
@@ -62,7 +82,7 @@ public class ScreenStackFragment extends ScreenFragment {
}
private CoordinatorLayout configureView() {
CoordinatorLayout view = new CoordinatorLayout(getContext());
CoordinatorLayout view = new NotifyingCoordinatorLayout(getContext(), this);
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
params.setBehavior(new AppBarLayout.ScrollingViewBehavior());
@@ -86,6 +106,30 @@ public class ScreenStackFragment extends ScreenFragment {
return view;
}
@Override
public void onViewAnimationEnd() {
super.onViewAnimationEnd();
notifyViewAppearTransitionEnd();
}
@Nullable
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (enter && transit == 0) {
// this means that the fragment will appear without transition, in this case onViewAnimationEnd
// won't be called and we need to notify stack directly from here.
notifyViewAppearTransitionEnd();
}
return null;
}
private void notifyViewAppearTransitionEnd() {
ViewParent screenStack = getView().getParent();
if (screenStack instanceof ScreenStack) {
((ScreenStack) screenStack).onViewAppearTransitionEnd();
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@@ -94,7 +138,7 @@ public class ScreenStackFragment extends ScreenFragment {
mScreenRootView = configureView();
}
return mScreenRootView;
return recycleView(mScreenRootView);
}
public boolean isDismissable() {

View File

@@ -132,6 +132,11 @@ public class ScreenStackHeaderConfig extends ViewGroup {
return;
}
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
if (activity == null) {
return;
}
if (mIsHidden) {
if (mToolbar.getParent() != null) {
getScreenFragment().removeToolbar();
@@ -143,7 +148,6 @@ public class ScreenStackHeaderConfig extends ViewGroup {
getScreenFragment().setToolbar(mToolbar);
}
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
activity.setSupportActionBar(mToolbar);
ActionBar actionBar = activity.getSupportActionBar();

View File

@@ -35,7 +35,7 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
public void setStackPresentation(Screen view, String presentation) {
if ("push".equals(presentation)) {
view.setStackPresentation(Screen.StackPresentation.PUSH);
} else if ("modal".equals(presentation) || "containedModal".equals(presentation)) {
} else if ("modal".equals(presentation) || "containedModal".equals(presentation) || "fullScreenModal".equals(presentation) || "formSheet".equals(presentation)) {
// at the moment Android implementation does not handle contained vs regular modals
view.setStackPresentation(Screen.StackPresentation.MODAL);
} else if ("transparentModal".equals(presentation) || "containedTransparentModal".equals((presentation))) {
@@ -69,6 +69,8 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"),
ScreenAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onAppear"));
MapBuilder.of("registrationName", "onAppear"),
StackFinishTransitioningEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onFinishTransitioning"));
}
}

View File

@@ -0,0 +1,29 @@
package com.swmansion.rnscreens;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class StackFinishTransitioningEvent extends Event<ScreenAppearEvent> {
public static final String EVENT_NAME = "topFinishTransitioning";
public StackFinishTransitioningEvent(int viewId) {
super(viewId);
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public short getCoalescingKey() {
// All events for a given view can be coalesced.
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap());
}
}

View File

@@ -30,15 +30,31 @@ class StackView extends React.Component {
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
};
_onSceneFocus = (route, descriptor) => {
_onAppear = (route, descriptor) => {
descriptor.options &&
descriptor.options.onAppear &&
descriptor.options.onAppear();
this.props.navigation.dispatch(
StackActions.completeTransition({ toChildKey: route.key })
StackActions.completeTransition({
toChildKey: route.key,
key: this.props.navigation.state.key,
})
);
};
_onFinishTransitioning = () => {
const { routes } = this.props.navigation.state;
let lastRoute = routes && routes.length && routes[routes.length - 1];
if (lastRoute) {
this.props.navigation.dispatch(
StackActions.completeTransition({
toChildKey: lastRoute.key,
key: this.props.navigation.state.key,
})
);
}
};
_renderHeaderConfig = (index, route, descriptor) => {
const { navigationConfig } = this.props;
const { options } = descriptor;
@@ -188,13 +204,13 @@ class StackView extends React.Component {
return (
<Screen
key={`screen_${route.key}`}
style={options.cardStyle}
style={[StyleSheet.absoluteFill, options.cardStyle]}
stackAnimation={stackAnimation}
stackPresentation={stackPresentation}
gestureEnabled={
options.gestureEnabled === undefined ? true : options.gestureEnabled
}
onAppear={() => this._onSceneFocus(route, descriptor)}
onAppear={() => this._onAppear(route, descriptor)}
onDismissed={() => this._removeScene(route)}>
{this._renderHeaderConfig(index, route, descriptor)}
<SceneView
@@ -210,7 +226,9 @@ class StackView extends React.Component {
const { navigation, descriptors } = this.props;
return (
<ScreenStack style={styles.scenes}>
<ScreenStack
style={styles.scenes}
onFinishTransitioning={this._onFinishTransitioning}>
{navigation.state.routes.map((route, i) =>
this._renderScene(i, route, descriptors[route.key])
)}

View File

@@ -10,7 +10,9 @@ typedef NS_ENUM(NSInteger, RNSScreenStackPresentation) {
RNSScreenStackPresentationModal,
RNSScreenStackPresentationTransparentModal,
RNSScreenStackPresentationContainedModal,
RNSScreenStackPresentationContainedTransparentModal
RNSScreenStackPresentationContainedTransparentModal,
RNSScreenStackPresentationFullScreenModal,
RNSScreenStackPresentationFormSheet
};
typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {

View File

@@ -8,7 +8,7 @@
#import <React/RCTShadowView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate>
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate, RCTInvalidating>
@end
@implementation RNSScreenView {
@@ -34,11 +34,21 @@
- (void)reactSetFrame:(CGRect)frame
{
if (_active) {
if (![self.reactViewController.parentViewController
isKindOfClass:[UINavigationController class]]) {
[super reactSetFrame:frame];
}
// ignore setFrame call from react, the frame of this view
// is controlled by the UIViewController it is contained in
// when screen is mounted under UINavigationController it's size is controller
// by the navigation controller itself. That is, it is set to fill space of
// the controller. In that case we ignore react layout system from managing
// the screen dimentions and we wait for the screen VC to update and then we
// pass the dimentions to ui view manager to take into account when laying out
// subviews
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)updateBounds
@@ -62,7 +72,6 @@
- (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
{
_stackPresentation = stackPresentation;
switch (stackPresentation) {
case RNSScreenStackPresentationModal:
#ifdef __IPHONE_13_0
@@ -75,6 +84,12 @@
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
#endif
break;
case RNSScreenStackPresentationFullScreenModal:
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
break;
case RNSScreenStackPresentationFormSheet:
_controller.modalPresentationStyle = UIModalPresentationFormSheet;
break;
case RNSScreenStackPresentationTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverFullScreen;
break;
@@ -84,11 +99,23 @@
case RNSScreenStackPresentationContainedTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverCurrentContext;
break;
case RNSScreenStackPresentationPush:
// ignored, we only need to keep in mind not to set presentation delegate
break;
}
// `modalPresentationStyle` must be set before accessing `presentationController`
// otherwise a default controller will be created and cannot be changed after.
// Documented here: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
_controller.presentationController.delegate = self;
// There is a bug in UIKit which causes retain loop when presentationController is accessed for a
// controller that is not going to be presented modally. We therefore need to avoid setting the
// delegate for screens presented using push. This also means that when controller is updated from
// modal to push type, this may cause memory leak, we warn about that as well.
if (stackPresentation != RNSScreenStackPresentationPush) {
// `modalPresentationStyle` must be set before accessing `presentationController`
// otherwise a default controller will be created and cannot be changed after.
// Documented here: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
_controller.presentationController.delegate = self;
} else if (_stackPresentation != RNSScreenStackPresentationPush) {
RCTLogError(@"Screen presentation updated from modal to push, this may likely result in a screen object leakage. If you need to change presentation style create a new screen object instead");
}
_stackPresentation = stackPresentation;
}
- (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
@@ -109,6 +136,17 @@
}
}
- (void)setGestureEnabled:(BOOL)gestureEnabled
{
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
_controller.modalInPresentation = !gestureEnabled;
}
#endif
_gestureEnabled = gestureEnabled;
}
- (UIView *)reactSuperview
{
return _reactSuperview;
@@ -191,10 +229,27 @@
[_touchHandler reset];
}
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
return _gestureEnabled;
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:)
withObject:presentationController];
}
}
- (void)invalidate
{
_controller = nil;
}
@end
@implementation RNSScreen {
__weak UIView *_view;
__weak id _previousFirstResponder;
CGRect _lastViewFrame;
}
@@ -202,7 +257,7 @@
- (instancetype)initWithView:(UIView *)view
{
if (self = [super init]) {
_view = view;
self.view = view;
}
return self;
}
@@ -213,7 +268,7 @@
if (!CGRectEqualToRect(_lastViewFrame, self.view.frame)) {
_lastViewFrame = self.view.frame;
[((RNSScreenView *)self.view) updateBounds];
[((RNSScreenView *)self.viewIfLoaded) updateBounds];
}
}
@@ -233,6 +288,7 @@
- (void)willMoveToParentViewController:(UIViewController *)parent
{
[super willMoveToParentViewController:parent];
if (parent == nil) {
id responder = [self findFirstResponder:self.view];
if (responder != nil) {
@@ -262,14 +318,6 @@
_previousFirstResponder = nil;
}
- (void)loadView
{
if (_view != nil) {
self.view = _view;
_view = nil;
}
}
@end
@implementation RNSScreenManager
@@ -295,6 +343,8 @@ RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
RCT_ENUM_CONVERTER(RNSScreenStackPresentation, (@{
@"push": @(RNSScreenStackPresentationPush),
@"modal": @(RNSScreenStackPresentationModal),
@"fullScreenModal": @(RNSScreenStackPresentationFullScreenModal),
@"formSheet": @(RNSScreenStackPresentationFormSheet),
@"containedModal": @(RNSScreenStackPresentationContainedModal),
@"transparentModal": @(RNSScreenStackPresentationTransparentModal),
@"containedTransparentModal": @(RNSScreenStackPresentationContainedTransparentModal)

View File

@@ -11,7 +11,7 @@
@end
@interface RNSScreenContainerView ()
@interface RNSScreenContainerView () <RCTInvalidating>
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic, retain) NSMutableSet<RNSScreenView *> *activeScreens;
@@ -68,6 +68,11 @@
return _reactSubviews;
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)detachScreen:(RNSScreenView *)screen
{
[screen.controller willMoveToParentViewController:nil];
@@ -79,6 +84,9 @@
- (void)attachScreen:(RNSScreenView *)screen
{
[_controller addChildViewController:screen.controller];
// the frame is already set at this moment because we adjust it in insertReactSubview. We don't
// want to update it here as react-driven animations may already run and hence changing the frame
// would result in visual glitches
[_controller.view addSubview:screen.controller.view];
[screen.controller didMoveToParentViewController:_controller];
[_activeScreens addObject:screen];
@@ -155,10 +163,22 @@
[self markChildUpdated];
}
- (void)didMoveToWindow
{
if (self.window) {
[self reactAddControllerToClosestParent:_controller];
}
}
- (void)invalidate
{
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self reactAddControllerToClosestParent:_controller];
_controller.view.frame = self.bounds;
for (RNSScreenView *subview in _reactSubviews) {
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);

View File

@@ -4,6 +4,8 @@
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate, RCTInvalidating>
@property (nonatomic, copy) RCTDirectEventBlock onFinishTransitioning;
- (void)markChildUpdated;
- (void)didUpdateChildren;

View File

@@ -9,7 +9,12 @@
#import <React/RCTRootContentView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate, UIGestureRecognizerDelegate>
@property (nonatomic) NSMutableArray<UIViewController *> *presentedModals;
@property (nonatomic) BOOL updatingModals;
@property (nonatomic) BOOL scheduleModalsUpdate;
@end
@interface RNSScreenStackAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@@ -17,12 +22,9 @@
@end
@implementation RNSScreenStackView {
BOOL _needUpdate;
UINavigationController *_controller;
NSMutableArray<RNSScreenView *> *_reactSubviews;
NSMutableSet<RNSScreenView *> *_dismissedScreens;
NSMutableArray<UIViewController *> *_presentedModals;
__weak UIViewController* recentPopped;
__weak RNSScreenStackManager *_manager;
}
@@ -35,8 +37,6 @@
_dismissedScreens = [NSMutableSet new];
_controller = [[UINavigationController alloc] init];
_controller.delegate = self;
_needUpdate = NO;
[self addSubview:_controller.view];
_controller.interactivePopGestureRecognizer.delegate = self;
// we have to initialize viewControllers with a non empty array for
@@ -48,6 +48,11 @@
return self;
}
- (UIViewController *)reactViewController
{
return _controller;
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
UIView *view = viewController.view;
@@ -64,15 +69,36 @@
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
for (NSUInteger i = _reactSubviews.count; i > 0; i--) {
if ([viewController isEqual:[_reactSubviews objectAtIndex:i - 1].controller]) {
RNSScreenView *screenView = [_reactSubviews objectAtIndex:i - 1];
if ([viewController isEqual:screenView.controller]) {
break;
} else {
[_dismissedScreens addObject:[_reactSubviews objectAtIndex:i - 1]];
} else if (screenView.stackPresentation == RNSScreenStackPresentationPush) {
[_dismissedScreens addObject:screenView];
}
}
if (recentPopped != nil) {
recentPopped.view = nil;
recentPopped = nil;
if (self.onFinishTransitioning) {
self.onFinishTransitioning(nil);
}
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
// We don't directly set presentation delegate but instead rely on the ScreenView's delegate to
// forward certain calls to the container (Stack).
UIView *screenView = presentationController.presentedViewController.view;
if ([screenView isKindOfClass:[RNSScreenView class]]) {
[_dismissedScreens addObject:(RNSScreenView *)screenView];
[_presentedModals removeObject:presentationController.presentedViewController];
if (self.onFinishTransitioning) {
// instead of directly triggering onFinishTransitioning this time we enqueue the event on the
// main queue. We do that because onDismiss event is also enqueued and we want for the transition
// finish event to arrive later than onDismiss (see RNSScreen#notifyDismiss)
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onFinishTransitioning) {
self.onFinishTransitioning(nil);
}
});
}
}
}
@@ -83,7 +109,6 @@
screen = (RNSScreenView *) toVC.view;
} else if (operation == UINavigationControllerOperationPop) {
screen = (RNSScreenView *) fromVC.view;
recentPopped = fromVC;
}
if (screen != nil && (screen.stackAnimation == RNSScreenStackAnimationFade || screen.stackAnimation == RNSScreenStackAnimationNone)) {
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
@@ -123,11 +148,13 @@
RCTLogError(@"ScreenStack only accepts children of type Screen");
return;
}
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
}
- (void)removeReactSubview:(RNSScreenView *)subview
{
subview.reactSuperview = nil;
[_reactSubviews removeObject:subview];
[_dismissedScreens removeObject:subview];
}
@@ -139,8 +166,48 @@
- (void)didUpdateReactSubviews
{
// do nothing
[self updateContainer];
// we need to wait until children have their layout set. At this point they don't have the layout
// set yet, however the layout call is already enqueued on ui thread. Enqueuing update call on the
// ui queue will guarantee that the update will run after layout.
dispatch_async(dispatch_get_main_queue(), ^{
[self updateContainer];
});
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (self.window) {
// when stack is attached to a window we do two things:
// 1) we run updateContainer we do this because we want push view controllers to be installed
// before the VC is mounted. If we do that after it is added to parent the push updates operations
// are going to be blocked by UIKit.
// 2) we add navigation VS to parent this is needed for the VC lifecycle events to be dispatched
// properly
// 3) we again call updateContainer this time we do this to open modal controllers. Modals
// won't open in (1) because they require navigator to be added to parent. We handle that case
// gracefully in setModalViewControllers and can retry opening at any point.
[self updateContainer];
[self reactAddControllerToClosestParent:_controller];
[self updateContainer];
}
}
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
{
if (!controller.parentViewController) {
UIView *parentView = (UIView *)self.reactSuperview;
while (parentView) {
if (parentView.reactViewController) {
[parentView.reactViewController addChildViewController:controller];
[self addSubview:controller.view];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
- (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
@@ -176,14 +243,58 @@
}
}
// prevent re-entry
if (_updatingModals) {
_scheduleModalsUpdate = YES;
return;
}
_updatingModals = YES;
__weak RNSScreenStackView *weakSelf = self;
void (^afterTransitions)(void) = ^{
if (weakSelf.onFinishTransitioning) {
weakSelf.onFinishTransitioning(nil);
}
weakSelf.updatingModals = NO;
if (weakSelf.scheduleModalsUpdate) {
// if modals update was requested during setModalViewControllers we set scheduleModalsUpdate
// flag in order to perform updates at a later point. Here we are done with all modals
// transitions and check this flag again. If it was set, we reset the flag and execute updates.
weakSelf.scheduleModalsUpdate = NO;
[weakSelf updateContainer];
}
};
void (^finish)(void) = ^{
UIViewController *previous = changeRootController;
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
UIViewController *next = controllers[i];
[previous presentViewController:next
animated:(i == controllers.count - 1)
completion:nil];
previous = next;
NSUInteger oldCount = weakSelf.presentedModals.count;
if (changeRootIndex < oldCount) {
[weakSelf.presentedModals
removeObjectsInRange:NSMakeRange(changeRootIndex, oldCount - changeRootIndex)];
}
BOOL isAttached = changeRootController.parentViewController != nil || changeRootController.presentingViewController != nil;
if (!isAttached || changeRootIndex >= controllers.count) {
// if change controller view is not attached, presenting modals will silently fail on iOS.
// In such a case we trigger controllers update from didMoveToWindow.
// We also don't run any present transitions if changeRootIndex is greater or equal to the size
// of new controllers array. This means that no new controllers should be presented.
afterTransitions();
return;
} else {
UIViewController *previous = changeRootController;
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
UIViewController *next = controllers[i];
BOOL lastModal = (i == controllers.count - 1);
[previous presentViewController:next
animated:lastModal
completion:^{
[weakSelf.presentedModals addObject:next];
if (lastModal) {
afterTransitions();
};
}];
previous = next;
}
}
};
@@ -194,7 +305,6 @@
} else {
finish();
}
[_presentedModals setArray:controllers];
}
- (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
@@ -204,6 +314,12 @@
return;
}
// if view controller is not yet attached to window we skip updates now and run them when view
// is attached
if (self.window == nil) {
return;
}
UIViewController *top = controllers.lastObject;
UIViewController *lastTop = _controller.viewControllers.lastObject;
@@ -213,7 +329,7 @@
// controller is still there
BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreen class]];
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone && !_controller.presentedViewController;
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
if (firstTimePush) {
// nothing pushed yet
@@ -251,7 +367,7 @@
NSMutableArray<UIViewController *> *pushControllers = [NSMutableArray new];
NSMutableArray<UIViewController *> *modalControllers = [NSMutableArray new];
for (RNSScreenView *screen in _reactSubviews) {
if (![_dismissedScreens containsObject:screen]) {
if (![_dismissedScreens containsObject:screen] && screen.controller != nil) {
if (pushControllers.count == 0) {
// first screen on the list needs to be places as "push controller"
[pushControllers addObject:screen.controller];
@@ -272,7 +388,6 @@
- (void)layoutSubviews
{
[super layoutSubviews];
[self reactAddControllerToClosestParent:_controller];
_controller.view.frame = self.bounds;
}
@@ -282,6 +397,8 @@
[controller dismissViewControllerAnimated:NO completion:nil];
}
[_presentedModals removeAllObjects];
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
- (void)dismissOnReload
@@ -299,8 +416,7 @@
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(transitioning, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(onFinishTransitioning, RCTDirectEventBlock);
- (UIView *)view
{

View File

@@ -118,9 +118,12 @@
UINavigationBar *navbar = ((UINavigationController *)vc.parentViewController).navigationBar;
[navbar setTintColor:config.color];
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
// font customized on the navigation item level, so nothing to do here
} else {
} else
#endif
{
BOOL hideShadow = config.hideShadow;
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
@@ -165,15 +168,6 @@
[navbar setLargeTitleTextAttributes:largeAttrs];
}
}
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
if (backButtonImage) {
navbar.backIndicatorImage = backButtonImage;
navbar.backIndicatorTransitionMaskImage = backButtonImage;
} else if (navbar.backIndicatorImage) {
navbar.backIndicatorImage = nil;
navbar.backIndicatorTransitionMaskImage = nil;
}
}
}
@@ -196,6 +190,20 @@
if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) {
hasBackButtonImage = YES;
RCTImageView *imageView = subview.subviews[0];
if (imageView.image == nil) {
// This is yet another workaround for loading custom back icon. It turns out that under
// certain circumstances image attribute can be null despite the app running in production
// mode (when images are loaded from the filesystem). This can happen because image attribute
// is reset when image view is detached from window, and also in some cases initialization
// does not populate the frame of the image view before the loading start. The latter result
// in the image attribute not being updated. We manually set frame to the size of an image
// in order to trigger proper reload that'd update the image attribute.
RCTImageSource *source = imageView.imageSources[0];
[imageView reactSetFrame:CGRectMake(imageView.frame.origin.x,
imageView.frame.origin.y,
source.size.width,
source.size.height)];
}
UIImage *image = imageView.image;
// IMPORTANT!!!
// image can be nil in DEV MODE ONLY
@@ -267,11 +275,7 @@
}
[navctr setNavigationBarHidden:shouldHide animated:YES];
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
vc.modalInPresentation = !config.screenView.gestureEnabled;
}
#endif
if (shouldHide) {
return;
}
@@ -375,8 +379,20 @@
navitem.standardAppearance = appearance;
navitem.compactAppearance = appearance;
navitem.scrollEdgeAppearance = appearance;
}
} else
#endif
{
// updating backIndicatotImage does not work when called during transition. On iOS pre 13 we need
// to update it before the navigation starts.
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
if (backButtonImage) {
navctr.navigationBar.backIndicatorImage = backButtonImage;
navctr.navigationBar.backIndicatorTransitionMaskImage = backButtonImage;
} else if (navctr.navigationBar.backIndicatorImage) {
navctr.navigationBar.backIndicatorImage = nil;
navctr.navigationBar.backIndicatorTransitionMaskImage = nil;
}
}
navitem.hidesBackButton = config.hideBackButton;
navitem.leftBarButtonItem = nil;
navitem.rightBarButtonItem = nil;

View File

@@ -1,7 +1,7 @@
{
"name": "react-native-screens",
"version": "2.0.0-beta.2",
"description": "First incomplete navigation solution for your react-native app.",
"version": "2.0.0-beta.13",
"description": "Native navigation primitives for your React Native app.",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "npm run format && npm run lint && npm run test:unit",

2
src/screens.d.ts vendored
View File

@@ -15,7 +15,7 @@ declare module 'react-native-screens' {
export function enableScreens(shouldEnableScreens?: boolean): void;
export function screensEnabled(): boolean;
export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal';
export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal' | 'fullScreenModal' | 'formSheet';
export type StackAnimationTypes = 'default' | 'fade' | 'flip' | 'none';
export interface ScreenProps extends ViewProps {