From a085f77260dbc03d8317b554ec7a8679714b4e7d Mon Sep 17 00:00:00 2001 From: James Ide Date: Thu, 19 Feb 2015 16:08:14 -0800 Subject: [PATCH] [Nav] Add support for bar button icons and left buttons NavigatorIOS supports four new properties: - **rightButtonImageSource:** The source of an image to display in the top right. This must be a static image since UINavigationController only supports UIImages. Adding support for UIImageViews (or arbitrary views) is more complicated because custom views do not fade on touch and do not have hit slop the same way that UIImage buttons do. Usage: `rightButtonImageSource: ix('ImageName')` - **backButtonImageSource:** Use a custom image for the back button. This does not replace the back caret (`<`) but instead replaces the text next to it. - **leftButtonTitle**: Text for the left nav button, which supersedes the previous nav item's back button when specified. The main use case for this is your initial screen/UIVC which has nothing to go back to (since it is the first VC on the stack) but need to display a left button. This does hide the back button if there would have been one otherwise. - **leftButtonImageSource:** Image source for the left button, supersedes the left button title. Added UIExplorer example to demonstrate. --- Examples/UIExplorer/ImageMocks.js | 5 ++ Examples/UIExplorer/NavigatorIOSExample.js | 25 ++++++++++ .../NavBarButtonPlus.imageset/Contents.json | 21 ++++++++ .../NavBarButtonPlus@3x.png | Bin 0 -> 348 bytes .../story-background.imageset/Contents.json | 4 ++ .../Components/Navigation/NavigatorIOS.ios.js | 47 ++++++++++++++++++ React/Modules/RCTUIManager.m | 6 +++ React/Views/RCTNavItem.h | 4 ++ React/Views/RCTNavItemManager.m | 13 +++++ React/Views/RCTWrapperViewController.m | 46 ++++++++++++++--- 10 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json create mode 100644 Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js index b888acbf74ca41..3f1883fa65c5bb 100644 --- a/Examples/UIExplorer/ImageMocks.js +++ b/Examples/UIExplorer/ImageMocks.js @@ -39,3 +39,8 @@ declare module 'image!uie_thumb_selected' { declare var uri: string; declare var isStatic: boolean; } + +declare module 'image!NavBarButtonPlus' { + declare var uri: string; + declare var isStatic: boolean; +} diff --git a/Examples/UIExplorer/NavigatorIOSExample.js b/Examples/UIExplorer/NavigatorIOSExample.js index a7acdde24d08ed..8cda0966edb42a 100644 --- a/Examples/UIExplorer/NavigatorIOSExample.js +++ b/Examples/UIExplorer/NavigatorIOSExample.js @@ -18,6 +18,7 @@ var React = require('react-native'); var ViewExample = require('./ViewExample'); var { + AlertIOS, PixelRatio, ScrollView, StyleSheet, @@ -91,6 +92,30 @@ var NavigatorIOSExample = React.createClass({ } }); })} + {this._renderRow('Custom Left & Right Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonTitle: 'Custom Left', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonImageSource: require('image!NavBarButtonPlus'), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} {this._renderRow('Pop', () => { this.props.navigator.pop(); })} diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json new file mode 100644 index 00000000000000..8af814b687aebd --- /dev/null +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "NavBarButtonPlus@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..551cea0d08693d0e34f2cd6b8d70f2642791c478 GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^#vshW1|+Q(8fXD2#^NA%Cx&(BWL^R}oCO|{#S9FJ z79h;%I?XTvC@7QZ;vWK}nSdAsq<~lhN=pLiC9|)tz5}ESJY5_^GFactaO7hU;9>du z|8mBomdk2)SM(fdToRT(XUjC}Up95~cXoCzf8Tp#*P8`1!>=Z?8M86j7&E76G8qe* zISZ!oOjycs#^VN$>kLDOX-WpliN+#{#~32J4)nC-ir#$@de{2j)>URg-~TP2$;iO) z|9{N-k0n66V?lPmow$*gK|!FU?*G!OjB7kH>}EvBxy_hq+5K+a`8@`=euiF`+kCM0 wn3>PdsSBUIiA~KpJSAQ->)gFiyh1A;a`*pzterOPA;_}~p00i_>zopr0Gl>}6aWAK literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json index 9c8120dffa3b0d..e1e9cd56bc0025 100644 --- a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json @@ -1,5 +1,9 @@ { "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, { "idiom" : "universal", "scale" : "2x", diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index aa5039edbfdb9c..794a615f19aa5c 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -12,6 +12,7 @@ 'use strict'; var EventEmitter = require('EventEmitter'); +var Image = require('Image'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTNavigatorManager = require('NativeModules').NavigatorManager; @@ -47,9 +48,14 @@ var RCTNavigatorItem = createReactIOSNativeComponentClass({ // NavigatorIOS does not use them all, because some are problematic title: true, barTintColor: true, + leftButtonImageName: true, + leftButtonTitle: true, + onNavLeftButtonTap: true, + rightButtonImageName: true, rightButtonTitle: true, onNavRightButtonTap: true, tintColor: true, + backButtonImageName: true, backButtonTitle: true, titleTextColor: true, style: true, @@ -78,7 +84,12 @@ type Route = { title: string; passProps: Object; backButtonTitle: string; + backButtonImageSource: Object; + leftButtonTitle: string; + leftButtonImageSource: Object; + onLeftButtonPress: Function; rightButtonTitle: string; + rightButtonImageSource: Object; onRightButtonPress: Function; wrapperStyle: any; }; @@ -211,6 +222,13 @@ var NavigatorIOS = React.createClass({ */ passProps: PropTypes.object, + /** + * If set, the left header button image will appear with this source. Note + * that this doesn't apply for the header of the current view, but the + * ones of the views that are pushed afterward. + */ + backButtonImageSource: Image.propTypes.source, + /** * If set, the left header button will appear with this name. Note that * this doesn't apply for the header of the current view, but the ones @@ -218,6 +236,26 @@ var NavigatorIOS = React.createClass({ */ backButtonTitle: PropTypes.string, + /** + * If set, the left header button image will appear with this source + */ + leftButtonImageSource: Image.propTypes.source, + + /** + * If set, the left header button will appear with this name + */ + leftButtonTitle: PropTypes.string, + + /** + * Called when the left header button is pressed + */ + onLeftButtonPress: PropTypes.func, + + /** + * If set, the right header button image will appear with this source + */ + rightButtonImageSource: Image.propTypes.source, + /** * If set, the right header button will appear with this name */ @@ -544,7 +582,12 @@ var NavigatorIOS = React.createClass({ this.props.itemWrapperStyle, route.wrapperStyle ]} + backButtonImageName={this._imageNameFromSource(route.backButtonImageSource)} backButtonTitle={route.backButtonTitle} + leftButtonImageName={this._imageNameFromSource(route.leftButtonImageSource)} + leftButtonTitle={route.leftButtonTitle} + onNavLeftButtonTap={route.onLeftButtonPress} + rightButtonImageName={this._imageNameFromSource(route.rightButtonImageSource)} rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} tintColor={this.props.tintColor}> @@ -558,6 +601,10 @@ var NavigatorIOS = React.createClass({ ); }, + _imageNameFromSource: function(source: ?Object) { + return source ? source.uri : undefined; + }, + renderNavigationStackItems: function() { var shouldRecurseToNavigator = this.state.makingNavigatorRequest || diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 185982fdcf205b..cea533551c586b 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1140,6 +1140,12 @@ - (NSDictionary *)customBubblingEventTypes @"captured": @"onNavigationCompleteCapture" } }, + @"topNavLeftButtonTap": @{ + @"phasedRegistrationNames": @{ + @"bubbled": @"onNavLeftButtonTap", + @"captured": @"onNavLefttButtonTapCapture" + } + }, @"topNavRightButtonTap": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onNavRightButtonTap", diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 6db429db27495d..5abd9ed3ded1ab 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -12,7 +12,11 @@ @interface RCTNavItem : UIView @property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) UIImage *leftButtonImage; +@property (nonatomic, copy) NSString *leftButtonTitle; +@property (nonatomic, copy) UIImage *rightButtonImage; @property (nonatomic, copy) NSString *rightButtonTitle; +@property (nonatomic, copy) UIImage *backButtonImage; @property (nonatomic, copy) NSString *backButtonTitle; @property (nonatomic, copy) UIColor *tintColor; @property (nonatomic, copy) UIColor *barTintColor; diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index 549859ae02c400..250e07be65d8f2 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -20,10 +20,23 @@ - (UIView *)view } RCT_EXPORT_VIEW_PROPERTY(title, NSString) +RCT_EXPORT_VIEW_PROPERTY(leftButtonTitle, NSString); RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString); RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString); RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor); +RCT_CUSTOM_VIEW_PROPERTY(leftButtonImageName, UIImage, RCTNavItem) +{ + view.leftButtonImage = json ? [RCTConvert UIImage:json] : defaultView.leftButtonImage; +} +RCT_CUSTOM_VIEW_PROPERTY(rightButtonImageName, UIImage, RCTNavItem) +{ + view.rightButtonImage = json ? [RCTConvert UIImage:json] : defaultView.rightButtonImage; +} +RCT_CUSTOM_VIEW_PROPERTY(backButtonImageName, UIImage, RCTNavItem) +{ + view.backButtonImage = json ? [RCTConvert UIImage:json] : defaultView.backButtonImage; +} @end diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 2008c1f83dff34..a3bbd6c85ec12f 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -86,17 +86,43 @@ - (void)viewWillAppear:(BOOL)animated [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; } - if (_navItem.rightButtonTitle.length > 0) { + if (_navItem.leftButtonImage) { + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithImage:_navItem.leftButtonImage + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavLeftButtonTapped)]; + } else if (_navItem.leftButtonTitle.length > 0) { + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithTitle:_navItem.leftButtonTitle + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavLeftButtonTapped)]; + } + + if (_navItem.rightButtonImage) { self.navigationItem.rightBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle - style:UIBarButtonItemStyleDone - target:self - action:@selector(handleNavRightButtonTapped)]; + [[UIBarButtonItem alloc] initWithImage:_navItem.rightButtonImage + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleNavRightButtonTapped)]; + } else if (_navItem.rightButtonTitle.length > 0) { + self.navigationItem.rightBarButtonItem = + [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle + style:UIBarButtonItemStyleDone + target:self + action:@selector(handleNavRightButtonTapped)]; } - if (_navItem.backButtonTitle.length > 0) { + if (_navItem.backButtonImage) { + self.navigationItem.backBarButtonItem = + [[UIBarButtonItem alloc] initWithImage:_navItem.backButtonImage + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_navItem.backButtonTitle.length > 0) { self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle + [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle style:UIBarButtonItemStylePlain target:nil action:nil]; @@ -114,6 +140,12 @@ - (void)loadView self.view = _wrapperView; } +- (void)handleNavLeftButtonTapped +{ + [_eventDispatcher sendInputEventWithName:@"topNavLeftButtonTap" + body:@{@"target":_navItem.reactTag}]; +} + - (void)handleNavRightButtonTapped { [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap"