Compare commits

..

13 Commits

Author SHA1 Message Date
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
15 changed files with 319 additions and 106 deletions

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) {
@@ -254,14 +220,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected void onDetachedFromWindow() {
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 +240,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);
@@ -312,7 +270,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,17 @@ 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);
}
return view;
}
protected Screen mScreenView;
public ScreenFragment() {
@@ -30,7 +42,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 +57,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,27 @@ 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
public void startAnimation(Animation animation) {
super.startAnimation(animation);
}
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
mFragment.onViewAnimationEnd();
}
}
private static final float TOOLBAR_ELEVATION = PixelUtil.toPixelFromDIP(4);
private AppBarLayout mAppBarLayout;
@@ -62,7 +87,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 +111,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 +143,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

@@ -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

@@ -34,11 +34,16 @@
- (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
}
- (void)updateBounds
@@ -191,6 +196,19 @@
[_touchHandler reset];
}
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
return _gestureEnabled;
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:)
withObject:presentationController];
}
}
@end
@implementation RNSScreen {
@@ -247,6 +265,8 @@
if (self.parentViewController == nil && self.presentingViewController == nil) {
// screen dismissed, send event
[((RNSScreenView *)self.view) notifyDismissed];
_view = self.view;
self.view = nil;
}
}

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,7 @@
#import <React/RCTRootContentView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate, UIGestureRecognizerDelegate>
@end
@interface RNSScreenStackAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@@ -22,7 +22,6 @@
NSMutableArray<RNSScreenView *> *_reactSubviews;
NSMutableSet<RNSScreenView *> *_dismissedScreens;
NSMutableArray<UIViewController *> *_presentedModals;
__weak UIViewController* recentPopped;
__weak RNSScreenStackManager *_manager;
}
@@ -64,15 +63,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 +103,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 +142,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];
}
@@ -176,15 +197,27 @@
}
}
__weak RNSScreenStackView *weakSelf = self;
void (^dispatchFinishTransitioning)(void) = ^{
if (weakSelf.onFinishTransitioning) {
weakSelf.onFinishTransitioning(nil);
}
};
void (^finish)(void) = ^{
UIViewController *previous = changeRootController;
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
UIViewController *next = controllers[i];
BOOL animate = (i == controllers.count - 1);
[previous presentViewController:next
animated:(i == controllers.count - 1)
completion:nil];
animated:animate
completion:animate ? dispatchFinishTransitioning : nil];
previous = next;
}
if (changeRootIndex >= controllers.count) {
dispatchFinishTransitioning();
}
};
if (changeRootController.presentedViewController) {
@@ -299,8 +332,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
@@ -375,8 +383,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,6 +1,6 @@
{
"name": "react-native-screens",
"version": "2.0.0-beta.2",
"version": "2.0.0-beta.6",
"description": "First incomplete navigation solution for your react-native app.",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",