From b97ce93ceae0dc4adc16706fbf054f14dcf208d0 Mon Sep 17 00:00:00 2001 From: James Ide Date: Thu, 7 May 2015 07:16:59 -0700 Subject: [PATCH] [Nav] Add support for bar button icons and left buttons Summary: 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, super Closes https://github.com/facebook/react-native/pull/263 Github Author: James Ide Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Examples/UIExplorer/ImageMocks.js | 5 + Examples/UIExplorer/NavigatorIOSExample.js | 25 +++++ .../NavBarButtonPlus.imageset/Contents.json | 21 ++++ .../NavBarButtonPlus@3x.png | Bin 0 -> 166 bytes .../Components/Navigation/NavigatorIOS.ios.js | 49 ++++++++- React/Modules/RCTUIManager.m | 6 ++ React/Views/RCTNavItem.h | 14 ++- React/Views/RCTNavItem.m | 101 +++++++++++++++++- React/Views/RCTNavItemManager.m | 20 ++-- React/Views/RCTWrapperViewController.m | 39 +++---- 10 files changed, 247 insertions(+), 33 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 b888acbf7..3f1883fa6 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 eee731bd5..4a2011a65 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(), + rightButtonIcon: 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 000000000..13726b4e9 --- /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", + "filename" : "NavBarButtonPlus@3x.png", + "scale" : "3x" + } + ], + "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..706a9c9052240e9c88d4aef09f70c8ad77f1edf3 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^#vshW1|+Q(8fXD2!4lVqlHmNblJdl&REC1Q^yH$_ z;tYpU&(A=~08bakkc@k8uWaOGaNuBZ{GER*|A0#+bE25uG|!$Jy4`bm+0Z~*(T7(F k2??_#)=Q$vusw6;@BjN)n|rEo2+(W>Pgg&ebxsLQ0Dm|t*8l(j literal 0 HcmV?d00001 diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 3babd1409..103e749f7 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, + leftButtonIcon: true, + leftButtonTitle: true, + onNavLeftButtonTap: true, + rightButtonIcon: true, rightButtonTitle: true, onNavRightButtonTap: true, + backButtonIcon: 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; + backButtonIcon: Object; + leftButtonTitle: string; + leftButtonIcon: Object; + onLeftButtonPress: Function; rightButtonTitle: string; + rightButtonIcon: 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. + */ + backButtonIcon: 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 + */ + leftButtonIcon: 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 + */ + rightButtonIcon: Image.propTypes.source, + /** * If set, the right header button will appear with this name */ @@ -560,7 +598,12 @@ var NavigatorIOS = React.createClass({ this.props.itemWrapperStyle, route.wrapperStyle ]} + backButtonIcon={this._imageNameFromSource(route.backButtonIcon)} backButtonTitle={route.backButtonTitle} + leftButtonIcon={this._imageNameFromSource(route.leftButtonIcon)} + leftButtonTitle={route.leftButtonTitle} + onNavLeftButtonTap={route.onLeftButtonPress} + rightButtonIcon={this._imageNameFromSource(route.rightButtonIcon)} rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} navigationBarHidden={this.props.navigationBarHidden} @@ -577,6 +620,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 d35cae03a..eac3a7735 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1159,6 +1159,12 @@ RCT_EXPORT_METHOD(clearJSResponder) @"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 5ae874522..cd9833a44 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -12,11 +12,19 @@ @interface RCTNavItem : UIView @property (nonatomic, copy) NSString *title; +@property (nonatomic, strong) UIImage *leftButtonIcon; +@property (nonatomic, copy) NSString *leftButtonTitle; +@property (nonatomic, strong) UIImage *rightButtonIcon; @property (nonatomic, copy) NSString *rightButtonTitle; +@property (nonatomic, strong) UIImage *backButtonIcon; @property (nonatomic, copy) NSString *backButtonTitle; @property (nonatomic, assign) BOOL navigationBarHidden; -@property (nonatomic, copy) UIColor *tintColor; -@property (nonatomic, copy) UIColor *barTintColor; -@property (nonatomic, copy) UIColor *titleTextColor; +@property (nonatomic, strong) UIColor *tintColor; +@property (nonatomic, strong) UIColor *barTintColor; +@property (nonatomic, strong) UIColor *titleTextColor; + +@property (nonatomic, readonly) UIBarButtonItem *backButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *rightButtonItem; @end diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index 6b1e92f44..56346a363 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -11,5 +11,104 @@ @implementation RCTNavItem -@end +@synthesize backButtonItem = _backButtonItem; +@synthesize leftButtonItem = _leftButtonItem; +@synthesize rightButtonItem = _rightButtonItem; +- (void)setBackButtonTitle:(NSString *)backButtonTitle +{ + _backButtonTitle = backButtonTitle; + _backButtonItem = nil; +} + +- (void)setBackButtonIcon:(UIImage *)backButtonIcon +{ + _backButtonIcon = backButtonIcon; + _backButtonItem = nil; +} + +- (UIBarButtonItem *)backButtonItem +{ + if (!_backButtonItem) { + if (_backButtonIcon) { + _backButtonItem = [[UIBarButtonItem alloc] initWithImage:_backButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_backButtonTitle.length) { + _backButtonItem = [[UIBarButtonItem alloc] initWithTitle:_backButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _backButtonItem = nil; + } + } + return _backButtonItem; +} + +- (void)setLeftButtonTitle:(NSString *)leftButtonTitle +{ + _leftButtonTitle = leftButtonTitle; + _leftButtonItem = nil; +} + +- (void)setLeftButtonIcon:(UIImage *)leftButtonIcon +{ + _leftButtonIcon = leftButtonIcon; + _leftButtonIcon = nil; +} + +- (UIBarButtonItem *)leftButtonItem +{ + if (!_leftButtonItem) { + if (_leftButtonIcon) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_leftButtonTitle.length) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _leftButtonItem = nil; + } + } + return _leftButtonItem; +} + +- (void)setRightButtonTitle:(NSString *)rightButtonTitle +{ + _rightButtonTitle = rightButtonTitle; + _rightButtonItem = nil; +} + +- (void)setRightButtonIcon:(UIImage *)rightButtonIcon +{ + _rightButtonIcon = rightButtonIcon; + _rightButtonItem = nil; +} + +- (UIBarButtonItem *)rightButtonItem +{ + if (!_rightButtonItem) { + if (_rightButtonIcon) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_rightButtonTitle.length) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _rightButtonItem = nil; + } + } + return _rightButtonItem; +} + +@end diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index fc601632f..33588c938 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -21,12 +21,20 @@ RCT_EXPORT_MODULE() return [[RCTNavItem alloc] init]; } +RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(title, 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_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor) + +RCT_EXPORT_VIEW_PROPERTY(backButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString) + +RCT_EXPORT_VIEW_PROPERTY(leftButtonTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(leftButtonIcon, UIImage) + +RCT_EXPORT_VIEW_PROPERTY(rightButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString) @end diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 53c2f16a7..400ce5fab 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -64,7 +64,6 @@ // TODO: find a way to make this less-tightly coupled to navigation controller if ([self.parentViewController isKindOfClass:[UINavigationController class]]) { - [self.navigationController setNavigationBarHidden:_navItem.navigationBarHidden animated:animated]; @@ -73,33 +72,23 @@ return; } - self.navigationItem.title = _navItem.title; - UINavigationBar *bar = self.navigationController.navigationBar; - if (_navItem.barTintColor) { - bar.barTintColor = _navItem.barTintColor; - } - if (_navItem.tintColor) { - bar.tintColor = _navItem.tintColor; - } + bar.barTintColor = _navItem.barTintColor; + bar.tintColor = _navItem.tintColor; if (_navItem.titleTextColor) { [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; } - if (_navItem.rightButtonTitle.length > 0) { - self.navigationItem.rightBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle - style:UIBarButtonItemStyleDone - target:self - action:@selector(handleNavRightButtonTapped)]; + UINavigationItem *item = self.navigationItem; + item.title = _navItem.title; + item.backBarButtonItem = _navItem.backButtonItem; + if ((item.leftBarButtonItem = _navItem.leftButtonItem)) { + item.leftBarButtonItem.target = self; + item.leftBarButtonItem.action = @selector(handleNavLeftButtonTapped); } - - if (_navItem.backButtonTitle.length > 0) { - self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + if ((item.rightBarButtonItem = _navItem.rightButtonItem)) { + item.rightBarButtonItem.target = self; + item.rightBarButtonItem.action = @selector(handleNavRightButtonTapped); } } } @@ -114,6 +103,12 @@ self.view = _wrapperView; } +- (void)handleNavLeftButtonTapped +{ + [_eventDispatcher sendInputEventWithName:@"topNavLeftButtonTap" + body:@{@"target":_navItem.reactTag}]; +} + - (void)handleNavRightButtonTapped { [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap"