From 00ae88e7c7e4dbd65f2ab330082abb8f9c5da3dc 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 .../Components/Navigation/NavigatorIOS.ios.js | 49 +++++++++++++++++- React/Modules/RCTUIManager.m | 6 +++ React/Views/RCTNavItem.h | 4 ++ React/Views/RCTNavItemManager.m | 13 +++++ React/Views/RCTWrapperViewController.m | 46 +++++++++++++--- 9 files changed, 161 insertions(+), 8 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 eee731bd53834a..f98c2996efaf93 100644 --- a/Examples/UIExplorer/NavigatorIOSExample.js +++ b/Examples/UIExplorer/NavigatorIOSExample.js @@ -19,6 +19,7 @@ var React = require('react-native'); var ViewExample = require('./ViewExample'); var createExamplePage = require('./createExamplePage'); var { + AlertIOS, PixelRatio, ScrollView, StyleSheet, @@ -92,6 +93,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/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index fd90847f05c61f..27c914789511c4 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,11 +48,16 @@ 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, + backButtonImageName: true, + backButtonTitle: true, tintColor: true, navigationBarHidden: true, - backButtonTitle: true, titleTextColor: true, style: true, }, @@ -79,7 +85,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; }; @@ -212,6 +223,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 @@ -219,6 +237,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 */ @@ -550,7 +588,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} navigationBarHidden={this.props.navigationBarHidden} @@ -565,6 +608,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 b6a350dcd4964e..0c66d6e5f8071f 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1165,6 +1165,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 5ae874522e8a5d..127da77e94d6e1 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, assign) BOOL navigationBarHidden; @property (nonatomic, copy) UIColor *tintColor; diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index fc601632f4c4e4..b21ad70b56d69d 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -22,11 +22,24 @@ - (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(navigationBarHidden, BOOL); 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 f9f35163448f78..4c5acd812c7d70 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -89,17 +89,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]; @@ -117,6 +143,12 @@ - (void)loadView self.view = _wrapperView; } +- (void)handleNavLeftButtonTapped +{ + [_eventDispatcher sendInputEventWithName:@"topNavLeftButtonTap" + body:@{@"target":_navItem.reactTag}]; +} + - (void)handleNavRightButtonTapped { [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap"