diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js
new file mode 100644
index 00000000000000..e3dc291e55d422
--- /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',
+ selectedSegmentIndex: undefined
+ };
+ },
+
+ render() {
+ return (
+
+
+ Value: {this.state.value}
+
+
+ Index: {this.state.selectedSegmentIndex}
+
+
+
+ );
+ },
+
+ _onChange(event) {
+ this.setState({
+ selectedSegmentIndex: event.nativeEvent.selectedSegmentIndex,
+ });
+ },
+
+ _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 d2f4d6f0c858c8..0e34d5f059d2f1 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -43,6 +43,7 @@ var COMPONENTS = [
NavigatorExample,
require('./PickerExample'),
require('./ScrollViewExample'),
+ require('./SegmentedControlIOSExample'),
require('./SliderIOSExample'),
require('./SwitchExample'),
require('./TabBarExample'),
diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js
index dbb5dde835b5cf..7a254edaca8cc0 100644
--- a/IntegrationTests/IntegrationTestsApp.js
+++ b/IntegrationTests/IntegrationTestsApp.js
@@ -26,6 +26,7 @@ var TESTS = [
require('./TimersTest'),
require('./AsyncStorageTest'),
require('./SimpleSnapshotTest'),
+ require('./SegmentedControlIOSSnapshotTest'),
];
TESTS.forEach(
diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
index 578d3915f488da..0bd2603925cc15 100644
--- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
+++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
@@ -72,6 +72,11 @@ - (void)testSimpleSnapshot
[_runner runTest:_cmd module:@"SimpleSnapshotTest"];
}
+- (void)testSegmentedControlIOS
+{
+ [_runner runTest:_cmd module:@"SegmentedControlIOSSnapshotTest"];
+}
+
- (void)testZZZ_NotInRecordMode
{
RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit.");
diff --git a/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSegmentedControlIOS_1@2x.png b/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSegmentedControlIOS_1@2x.png
new file mode 100644
index 00000000000000..a762a88aa2ba65
Binary files /dev/null and b/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSegmentedControlIOS_1@2x.png differ
diff --git a/IntegrationTests/SegmentedControlIOSSnapshotTest.js b/IntegrationTests/SegmentedControlIOSSnapshotTest.js
new file mode 100644
index 00000000000000..22164ad92ea4df
--- /dev/null
+++ b/IntegrationTests/SegmentedControlIOSSnapshotTest.js
@@ -0,0 +1,64 @@
+/**
+ * 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.
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ View,
+ SegmentedControlIOS,
+} = React;
+
+var { TestModule } = React.addons;
+
+var SegmentedControlIOSSnapshotTest = React.createClass({
+ componentDidMount() {
+ if (!TestModule.verifySnapshot) {
+ throw new Error('TestModule.verifySnapshot not defined.');
+ }
+ requestAnimationFrame(() => TestModule.verifySnapshot(this.done));
+ },
+
+ done() {
+ TestModule.markTestCompleted();
+ },
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ testRow: {
+ marginBottom: 10,
+ }
+});
+
+module.exports = SegmentedControlIOSSnapshotTest;
diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js
new file mode 100644
index 00000000000000..fc0c780d10e338
--- /dev/null
+++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js
@@ -0,0 +1,143 @@
+/**
+ * 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
+ *
+ * This is a controlled component version of RCTSegmentedControl.
+ */
+'use strict';
+
+var NativeMethodsMixin = require('NativeMethodsMixin');
+var PropTypes = require('ReactPropTypes');
+var React = require('React');
+var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+
+var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass');
+var merge = require('merge');
+
+var SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol';
+
+type DefaultProps = {
+ values: Array;
+ disabled: boolean;
+};
+
+
+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
+ */
+ selectedSegmentIndex: PropTypes.number,
+
+ /**
+ * Used to style and layout the `SegmentedControl`. See `StyleSheet.js` and
+ * `ViewStylePropTypes.js` for more info.
+ */
+ style: View.propTypes.style,
+
+ /**
+ * 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 true the user won't be able to interact with the control.
+ * Default value is false.
+ */
+ disabled: 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: [],
+ disabled: false
+ };
+ },
+
+ _onChange: function(event: Event) {
+ this.props.onChange && this.props.onChange(event);
+ this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value);
+ },
+
+ render: function() {
+ var valuesAndSelectedSegmentIndex = {
+ selectedSegmentIndex: this.props.selectedSegmentIndex,
+ values: this.props.values
+ };
+ return (
+
+ );
+ }
+});
+
+
+var styles = StyleSheet.create({
+ segmentedControl: {
+ // Hard-coded to match UISegmentedControl#intrinsicContentSize.height
+ height: 28
+ },
+});
+
+var rkSegmentedControlAttributes = merge(ReactIOSViewAttributes.UIView, {
+ tintColor: true,
+ momentary: true,
+ enabled: true,
+ // Send both values simultaneously, to avoid race condition where
+ // `selectedSegmentIndex` is set before `values`
+ valuesAndSelectedSegmentIndex: true
+});
+
+
+var RCTSegmentedControl = createReactIOSNativeComponentClass({
+ validAttributes: rkSegmentedControlAttributes,
+ uiViewClassName: 'RCTSegmentedControl',
+});
+
+module.exports = SegmentedControlIOS;
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index c0bb989f494744..3d07ea34e2540f 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -17,6 +17,7 @@
//
// var ReactNative = {...require('React'), /* additions */}
//
+
var ReactNative = Object.assign(Object.create(require('React')), {
// Components
ActivityIndicatorIOS: require('ActivityIndicatorIOS'),
@@ -28,6 +29,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
PickerIOS: require('PickerIOS'),
Navigator: require('Navigator'),
ScrollView: require('ScrollView'),
+ SegmentedControlIOS: require('SegmentedControlIOS'),
SliderIOS: require('SliderIOS'),
SwitchIOS: require('SwitchIOS'),
TabBarIOS: require('TabBarIOS'),
diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj
index ba6ec3aaba120c..ad621f05742440 100644
--- a/React/React.xcodeproj/project.pbxproj
+++ b/React/React.xcodeproj/project.pbxproj
@@ -59,6 +59,8 @@
83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; };
83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; };
83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBACB1A6023D300E9B192 /* RCTConvert.m */; };
+ F8626E811ACAEB7300B02338 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F8626E7F1ACAEB7300B02338 /* RCTSegmentedControlManager.m */; };
+ F8DEFCE61ACAF38600799827 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = F8DEFCE51ACAF38600799827 /* RCTSegmentedControl.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -190,6 +192,10 @@
83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTouchHandler.m; sourceTree = ""; };
83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = ""; };
83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; };
+ F8626E7F1ACAEB7300B02338 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; };
+ F8626E801ACAEB7300B02338 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; };
+ F8DEFCE41ACAF38600799827 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; };
+ F8DEFCE51ACAF38600799827 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -275,6 +281,10 @@
13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */,
13B07FF91A6947C200A75B9A /* RCTScrollViewManager.m */,
13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */,
+ F8DEFCE41ACAF38600799827 /* RCTSegmentedControl.h */,
+ F8DEFCE51ACAF38600799827 /* RCTSegmentedControl.m */,
+ F8626E801ACAEB7300B02338 /* RCTSegmentedControlManager.h */,
+ F8626E7F1ACAEB7300B02338 /* RCTSegmentedControlManager.m */,
13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */,
13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */,
14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */,
@@ -462,6 +472,7 @@
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */,
13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */,
83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */,
+ F8626E811ACAEB7300B02338 /* RCTSegmentedControlManager.m in Sources */,
83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */,
58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */,
832348161A77A5AA00B55238 /* Layout.c in Sources */,
@@ -473,6 +484,7 @@
13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */,
137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */,
13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */,
+ F8DEFCE61ACAF38600799827 /* RCTSegmentedControl.m in Sources */,
13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */,
13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */,
1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */,
diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h
new file mode 100644
index 00000000000000..2812aba9c0821c
--- /dev/null
+++ b/React/Views/RCTSegmentedControl.h
@@ -0,0 +1,19 @@
+//
+// 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;
+
+- (void)setValuesAndSelectedSegmentIndex:(NSDictionary *)valuesAndSelectedValue;
+
+@end
diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m
new file mode 100644
index 00000000000000..b2a7a34b6eb44c
--- /dev/null
+++ b/React/Views/RCTSegmentedControl.m
@@ -0,0 +1,56 @@
+//
+// RCTSegmentedControl.m
+// React
+//
+// Created by Clay Allsopp on 3/31/15.
+// Copyright (c) 2015 Facebook. All rights reserved.
+//
+
+#import "RCTSegmentedControl.h"
+#import "UIView+React.h"
+#import "RCTEventDispatcher.h"
+
+@implementation RCTSegmentedControl
+{
+ RCTEventDispatcher *_eventDispatcher;
+}
+
+- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
+{
+ if ((self = [super initWithFrame:CGRectZero])) {
+ _eventDispatcher = eventDispatcher;
+ [self addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
+ }
+ return self;
+}
+
+
+- (void)setValuesAndSelectedSegmentIndex:(NSDictionary *)valuesAndSelectedSegmentIndex
+{
+ [self removeAllSegments];
+
+ NSArray *values = valuesAndSelectedSegmentIndex[@"values"];
+ NSNumber *selectedSegmentIndex = valuesAndSelectedSegmentIndex[@"selectedSegmentIndex"];
+
+ NSUInteger insertAtIndex = 0;
+ for (NSString *value in values) {
+ [self insertSegmentWithTitle:value atIndex:insertAtIndex animated:NO];
+ insertAtIndex += 1;
+ }
+
+ if (selectedSegmentIndex) {
+ [self setSelectedSegmentIndex:[selectedSegmentIndex integerValue]];
+ }
+}
+
+- (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 00000000000000..03647c72edb9bf
--- /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 00000000000000..e9108db86f69a5
--- /dev/null
+++ b/React/Views/RCTSegmentedControlManager.m
@@ -0,0 +1,28 @@
+/**
+ * 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 "RCTSegmentedControl.h"
+
+#import "RCTBridge.h"
+
+@implementation RCTSegmentedControlManager
+
+- (UIView *)view
+{
+ return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(valuesAndSelectedSegmentIndex, NSDictionary);
+RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor);
+RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL);
+RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL);
+
+@end