diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
index 23ff439c..90ef863f 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
@@ -8,6 +8,7 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.ActionBar;
@@ -15,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.views.text.ReactFontManager;
import java.util.ArrayList;
@@ -184,6 +186,17 @@ public class ScreenStackHeaderConfig extends ViewGroup {
ScreenStackHeaderSubview view = mConfigSubviews.get(i);
ScreenStackHeaderSubview.Type type = view.getType();
+ if (type == ScreenStackHeaderSubview.Type.BACK) {
+ // we special case BACK button header config type as we don't add it as a view into toolbar
+ // but instead just copy the drawable from imageview that's added as a first child to it.
+ View firstChild = view.getChildAt(0);
+ if (!(firstChild instanceof ImageView)) {
+ throw new JSApplicationIllegalArgumentException("Back button header config view should have Image as first child");
+ }
+ actionBar.setHomeAsUpIndicator(((ImageView) firstChild).getDrawable());
+ continue;
+ }
+
Toolbar.LayoutParams params =
new Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.java
index ab1f947a..0a0c9e09 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.java
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.java
@@ -18,7 +18,8 @@ public class ScreenStackHeaderSubview extends ReactViewGroup {
LEFT,
CENTER,
TITLE,
- RIGHT
+ RIGHT,
+ BACK
}
private int mReactWidth, mReactHeight;
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.java
index 96d38bd0..f19897ba 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.java
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.java
@@ -47,6 +47,8 @@ public class ScreenStackHeaderSubviewManager extends ReactViewManager {
view.setType(ScreenStackHeaderSubview.Type.TITLE);
} else if ("right".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.RIGHT);
+ } else if ("back".equals(type)) {
+ view.setType(ScreenStackHeaderSubview.Type.BACK);
}
}
}
diff --git a/createNativeStackNavigator.js b/createNativeStackNavigator.js
index 856692a5..32e4eac6 100644
--- a/createNativeStackNavigator.js
+++ b/createNativeStackNavigator.js
@@ -12,6 +12,7 @@ import {
ScreenStack,
Screen,
ScreenStackHeaderConfig,
+ ScreenStackHeaderBackButtonImage,
ScreenStackHeaderLeftView,
ScreenStackHeaderRightView,
ScreenStackHeaderTitleView,
@@ -93,6 +94,12 @@ class StackView extends React.Component {
const children = [];
+ if (options.backButtonImage) {
+ children.push(
+
+ );
+ }
+
if (options.headerLeft !== undefined) {
children.push(
diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h
index 6c9b90af..b99d963b 100644
--- a/ios/RNSScreenStackHeaderConfig.h
+++ b/ios/RNSScreenStackHeaderConfig.h
@@ -34,6 +34,7 @@
@end
typedef NS_ENUM(NSInteger, RNSScreenStackHeaderSubviewType) {
+ RNSScreenStackHeaderSubviewTypeBackButton,
RNSScreenStackHeaderSubviewTypeLeft,
RNSScreenStackHeaderSubviewTypeRight,
RNSScreenStackHeaderSubviewTypeTitle,
diff --git a/ios/RNSScreenStackHeaderConfig.m b/ios/RNSScreenStackHeaderConfig.m
index 3992b611..d34d5a4d 100644
--- a/ios/RNSScreenStackHeaderConfig.m
+++ b/ios/RNSScreenStackHeaderConfig.m
@@ -5,6 +5,20 @@
#import
#import
#import
+#import
+#import
+#import
+
+// Some RN private method hacking below. Couldn't figure out better way to access image data
+// of a given RCTImageView. See more comments in the code section processing SubviewTypeBackButton
+@interface RCTImageView (Private)
+- (UIImage*)image;
+@end
+
+@interface RCTImageLoader (Private)
+- (id)imageCache;
+@end
+
@interface RNSScreenHeaderItemMeasurements : NSObject
@property (nonatomic, readonly) CGSize headerSize;
@@ -30,6 +44,7 @@
@interface RNSScreenStackHeaderSubview : UIView
+@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, weak) UIView *reactSuperview;
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
@@ -150,6 +165,15 @@
[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;
+ }
}
}
@@ -164,6 +188,59 @@
}
}
++ (UIImage*)loadBackButtonImageInViewController:(UIViewController *)vc
+ withConfig:(RNSScreenStackHeaderConfig *)config
+{
+ BOOL hasBackButtonImage = NO;
+ for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) {
+ if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) {
+ hasBackButtonImage = YES;
+ RCTImageView *imageView = subview.subviews[0];
+ UIImage *image = imageView.image;
+ // IMPORTANT!!!
+ // image can be nil in DEV MODE ONLY
+ //
+ // It is so, because in dev mode images are loaded over HTTP from the packager. In that case
+ // we first check if image is already loaded in cache and if it is, we take it from cache and
+ // display immediately. Otherwise we wait for the transition to finish and retry updating
+ // header config.
+ // Unfortunately due to some problems in UIKit we cannot update the image while the screen
+ // transition is ongoing. This results in the settings being reset after the transition is done
+ // to the state from before the transition.
+ if (image == nil) {
+ // in DEV MODE we try to load from cache (we use private API for that as it is not exposed
+ // publically in headers).
+ RCTImageSource *source = imageView.imageSources[0];
+ image = [subview.bridge.imageLoader.imageCache
+ imageForUrl:source.request.URL.absoluteString
+ size:source.size
+ scale:source.scale
+ resizeMode:imageView.resizeMode];
+ }
+ if (image == nil) {
+ // This will be triggered if the image is not in the cache yet. What we do is we wait until
+ // the end of transition and run header config updates again. We could potentially wait for
+ // image on load to trigger, but that would require even more private method hacking.
+ if (vc.transitionCoordinator) {
+ [vc.transitionCoordinator animateAlongsideTransition:^(id _Nonnull context) {
+ // nothing, we just want completion
+ } completion:^(id _Nonnull context) {
+ // in order for new back button image to be loaded we need to trigger another change
+ // in back button props that'd make UIKit redraw the button. Otherwise the changes are
+ // not reflected. Here we change back button visibility which is then immediately restored
+ vc.navigationItem.hidesBackButton = YES;
+ [config updateViewControllerIfNeeded];
+ }];
+ }
+ return [UIImage new];
+ } else {
+ return image;
+ }
+ }
+ }
+ return nil;
+}
+
+ (void)willShowViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
{
[self updateViewController:vc withConfig:config];
@@ -201,7 +278,6 @@
}
navitem.title = config.title;
- navitem.hidesBackButton = config.hideBackButton;
if (config.backTitle != nil) {
prevItem.backBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:config.backTitle
@@ -290,11 +366,19 @@
appearance.largeTitleTextAttributes = largeAttrs;
}
+ UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
+ if (backButtonImage) {
+ [appearance setBackIndicatorImage:backButtonImage transitionMaskImage:backButtonImage];
+ } else if (appearance.backIndicatorImage) {
+ [appearance setBackIndicatorImage:nil transitionMaskImage:nil];
+ }
+
navitem.standardAppearance = appearance;
navitem.compactAppearance = appearance;
navitem.scrollEdgeAppearance = appearance;
}
#endif
+ navitem.hidesBackButton = config.hideBackButton;
navitem.leftBarButtonItem = nil;
navitem.rightBarButtonItem = nil;
navitem.titleView = nil;
@@ -378,6 +462,7 @@ RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
@implementation RCTConvert (RNSScreenStackHeader)
RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
+ @"back": @(RNSScreenStackHeaderSubviewTypeBackButton),
@"left": @(RNSScreenStackHeaderSubviewTypeLeft),
@"right": @(RNSScreenStackHeaderSubviewTypeRight),
@"title": @(RNSScreenStackHeaderSubviewTypeTitle),
@@ -386,9 +471,7 @@ RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
@end
-@implementation RNSScreenStackHeaderSubview {
- __weak RCTBridge *_bridge;
-}
+@implementation RNSScreenStackHeaderSubview
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
diff --git a/src/screens.native.js b/src/screens.native.js
index 26b52686..63335d57 100644
--- a/src/screens.native.js
+++ b/src/screens.native.js
@@ -4,6 +4,7 @@ import {
requireNativeComponent,
View,
UIManager,
+ Image,
StyleSheet,
} from 'react-native';
import { version } from 'react-native/Libraries/Core/ReactNativeVersion';
@@ -146,6 +147,14 @@ const styles = StyleSheet.create({
},
});
+const ScreenStackHeaderBackButtonImage = props => (
+
+
+
+);
+
const ScreenStackHeaderRightView = props => (