From 11f204748ddfd89990a7d8c3094836a0baaba44b Mon Sep 17 00:00:00 2001 From: Clay Allsopp Date: Wed, 29 Apr 2015 08:15:28 -0700 Subject: [PATCH] Add SegmentedControlIOS Summary: Fixes #534: ![screen shot 2015-03-31 at 7 52 10 pm](https://cloud.githubusercontent.com/assets/153704/6934038/742ddd34-d7e3-11e4-8f55-3eb7d9d3f1cd.png) ```jsx console.log(value) } /> ``` This only supports string-based segments, not images. Also doesn't support full customization (no separator images etc); I figure this is a good MVP to lock-down a basic API I also included a snapshot test case, but the images keep coming out funky. When I look at the sim, I see that the text labels show up for the selected segment, but the snapshot keeps coming out with no text on those segments. I tried forcing a delay, but same result. Is that explainable? Obviously happy to change anything about the API, code-style nitpicks, etc Closes https://github.com/facebook/react-native/pull/564 Github Author: Clay Allsopp Test Plan: Imported from GitHub, without a `Test Plan:` line. --- .../UIExplorer/SegmentedControlIOSExample.js | 169 ++++++++++++++++++ Examples/UIExplorer/UIExplorerList.js | 1 + .../SegmentedControlIOS.js | 120 +++++++++++++ Libraries/react-native/react-native.js | 1 + React/React.xcodeproj/project.pbxproj | 12 ++ React/Views/RCTSegmentedControl.h | 20 +++ React/Views/RCTSegmentedControl.m | 57 ++++++ React/Views/RCTSegmentedControlManager.h | 14 ++ React/Views/RCTSegmentedControlManager.m | 39 ++++ 9 files changed, 433 insertions(+) create mode 100644 Examples/UIExplorer/SegmentedControlIOSExample.js create mode 100644 Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js create mode 100644 React/Views/RCTSegmentedControl.h create mode 100644 React/Views/RCTSegmentedControl.m create mode 100644 React/Views/RCTSegmentedControlManager.h create mode 100644 React/Views/RCTSegmentedControlManager.m diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js new file mode 100644 index 000000000..119196d8f --- /dev/null +++ b/Examples/UIExplorer/SegmentedControlIOSExample.js @@ -0,0 +1,169 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + SegmentedControlIOS, + Text, + View, + StyleSheet +} = React; + +var BasicSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + } +}); + +var PreSelectedSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var MomentarySegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var DisabledSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + }, +}); + +var ColorSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + }, +}); + +var EventSegmentedControlExample = React.createClass({ + getInitialState() { + return { + values: ['One', 'Two', 'Three'], + value: 'Not selected', + selectedIndex: undefined + }; + }, + + render() { + return ( + + + Value: {this.state.value} + + + Index: {this.state.selectedIndex} + + + + ); + }, + + _onChange(event) { + this.setState({ + selectedIndex: event.nativeEvent.selectedIndex, + }); + }, + + _onValueChange(value) { + this.setState({ + value: value, + }); + } +}); + +var styles = StyleSheet.create({ + text: { + fontSize: 14, + textAlign: 'center', + fontWeight: '500', + margin: 10, + }, +}); + +exports.title = ''; +exports.displayName = 'SegmentedControlExample'; +exports.description = 'Native segmented control'; +exports.examples = [ + { + title: 'Segmented controls can have values', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can have a pre-selected value', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be momentary', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be disabled', + render(): ReactElement { return ; } + }, + { + title: 'Custom colors can be provided', + render(): ReactElement { return ; } + }, + { + title: 'Change events can be detected', + render(): ReactElement { return ; } + } +]; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 730106177..a10d291bd 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -43,6 +43,7 @@ var COMPONENTS = [ require('./NavigatorIOSExample'), require('./PickerIOSExample'), require('./ScrollViewExample'), + require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), require('./SwitchIOSExample'), require('./TabBarIOSExample'), diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js new file mode 100644 index 000000000..23d952776 --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SegmentedControlIOS + * @flow + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); + +var requireNativeComponent = require('requireNativeComponent'); +var verifyPropTypes = require('verifyPropTypes'); + +type DefaultProps = { + values: Array; + enabled: boolean; +}; + +var SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol'; + +type Event = Object; + +/** + * Use `SegmentedControlIOS` to render a UISegmentedControl iOS. + */ +var SegmentedControlIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The labels for the control's segment buttons, in order. + */ + values: PropTypes.arrayOf(PropTypes.string), + + /** + * The index in `props.values` of the segment to be pre-selected + */ + selectedIndex: PropTypes.number, + + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange: PropTypes.func, + + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange: PropTypes.func, + + /** + * If false the user won't be able to interact with the control. + * Default value is true. + */ + enabled: PropTypes.bool, + + /** + * Accent color of the control. + */ + tintColor: PropTypes.string, + + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ + momentary: PropTypes.bool + }, + + getDefaultProps: function(): DefaultProps { + return { + values: [], + enabled: true + }; + }, + + _onChange: function(event: Event) { + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + segmentedControl: { + height: NativeModules.SegmentedControlManager.ComponentHeight + }, +}); + +var RCTSegmentedControl = requireNativeComponent( + 'RCTSegmentedControl', + null +); +if (__DEV__) { + verifyPropTypes( + RCTSegmentedControl, + RCTSegmentedControl.viewConfig + ); +} + +module.exports = SegmentedControlIOS; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 01bff7eae..b94b172f3 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -27,6 +27,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), Navigator: require('Navigator'), + SegmentedControlIOS: require('SegmentedControlIOS'), ScrollView: require('ScrollView'), SliderIOS: require('SliderIOS'), SwitchIOS: require('SwitchIOS'), diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index fce2aae42..10867e9cb 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; 00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */; }; + 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; }; + 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; }; 13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */; }; 13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */; }; 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; @@ -84,6 +86,10 @@ 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; }; 00C1A2B11AC0B7E000E89A1C /* RCTDevMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDevMenu.h; sourceTree = ""; }; 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenu.m; sourceTree = ""; }; + 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; }; + 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; }; + 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; }; + 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; }; 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewControllerProtocol.h; sourceTree = ""; }; @@ -290,6 +296,10 @@ 58114A141AAE854800E7D092 /* RCTPickerManager.h */, 58114A151AAE854800E7D092 /* RCTPickerManager.m */, 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, + 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */, + 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */, + 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */, + 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */, 13B07FF61A6947C200A75B9A /* RCTScrollView.h */, 13B07FF71A6947C200A75B9A /* RCTScrollView.m */, 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */, @@ -489,6 +499,7 @@ 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, + 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, @@ -529,6 +540,7 @@ 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, + 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */, diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h new file mode 100644 index 000000000..8e6e1255e --- /dev/null +++ b/React/Views/RCTSegmentedControl.h @@ -0,0 +1,20 @@ +// +// RCTSegmentedControl.h +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +@class RCTEventDispatcher; + +@interface RCTSegmentedControl : UISegmentedControl + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, copy) NSArray *values; +@property (nonatomic, assign) NSInteger selectedIndex; + +@end diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m new file mode 100644 index 000000000..59e4cfb86 --- /dev/null +++ b/React/Views/RCTSegmentedControl.m @@ -0,0 +1,57 @@ +// +// RCTSegmentedControl.m +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTSegmentedControl.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" + +@implementation RCTSegmentedControl +{ + RCTEventDispatcher *_eventDispatcher; +} + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + _selectedIndex = self.selectedSegmentIndex; + [self addTarget:self action:@selector(onChange:) + forControlEvents:UIControlEventValueChanged]; + } + return self; +} + +- (void)setValues:(NSArray *)values +{ + _values = [values copy]; + [self removeAllSegments]; + for (NSString *value in values) { + [self insertSegmentWithTitle:value atIndex:self.numberOfSegments animated:NO]; + } + super.selectedSegmentIndex = _selectedIndex; +} + +- (void)setSelectedIndex:(NSInteger)selectedIndex +{ + _selectedIndex = selectedIndex; + super.selectedSegmentIndex = selectedIndex; +} + +- (void)onChange:(UISegmentedControl *)sender +{ + NSDictionary *event = @{ + @"target": self.reactTag, + @"value": [self titleForSegmentAtIndex:sender.selectedSegmentIndex], + @"selectedSegmentIndex": @(sender.selectedSegmentIndex) + }; + [_eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +@end diff --git a/React/Views/RCTSegmentedControlManager.h b/React/Views/RCTSegmentedControlManager.h new file mode 100644 index 000000000..03647c72e --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTSegmentedControlManager : RCTViewManager + +@end diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m new file mode 100644 index 000000000..d7e1156ff --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTSegmentedControlManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTSegmentedControl.h" + +@implementation RCTSegmentedControlManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(values, NSStringArray) +RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL) +RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) + +- (NSDictionary *)constantsToExport +{ + RCTSegmentedControl *view = [[RCTSegmentedControl alloc] init]; + return @{ + @"ComponentHeight": @(view.intrinsicContentSize.height), + }; +} + +@end