From 44f7a00e95af91dbdfc9feb4f097558b85a8917b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 4 Jan 2016 07:59:10 -0800 Subject: [PATCH] Cross platform PullToRefreshView component Summary: Both iOS and Android currently support some sort of native pull to refresh control but the API was very different. I tried implementing a component based on PullToRefreshViewAndroid but that works on both platforms. I liked the idea of wrapping the ListView or ScrollView with the PullToRefreshView component and allow styling the refresh view with platform specific props if needed. I also like the fact that 'refreshing' is a controlled prop so there is no need to keep a ref to the component or to the stopRefreshing function. It is a pretty rough start so I'm looking for feedback and ideas to improve on the API before cleaning up everything. On iOS we could probably deprecate the onRefreshStart property of the ScrollView and implement the native stuff in a PullToRefreshViewManager. We could then add props to customize the look of the UIRefreshControl (tintColor). We could also deprecate the Android only component and remove it later. Closes https://github.com/facebook/react-native/pull/4915 Reviewed By: svcscm Differential Revision: D2799246 Pulled By: nicklockwood fb-gh-sync-id: 75872c12143ddbc05cc91900ab4612e477ca5765 --- Examples/UIExplorer/RefreshControlExample.js | 125 ++++++++++++++++++ Examples/UIExplorer/UIExplorerList.android.js | 1 + Examples/UIExplorer/UIExplorerList.ios.js | 1 + .../RefreshControl/RefreshControl.js | 96 ++++++++++++++ Libraries/Components/ScrollView/ScrollView.js | 58 +++++--- Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 3 +- React/React.xcodeproj/project.pbxproj | 12 ++ React/Views/RCTRefreshControl.h | 19 +++ React/Views/RCTRefreshControl.m | 54 ++++++++ React/Views/RCTRefreshControlManager.h | 14 ++ React/Views/RCTRefreshControlManager.m | 28 ++++ React/Views/RCTScrollView.m | 24 +++- 13 files changed, 414 insertions(+), 22 deletions(-) create mode 100644 Examples/UIExplorer/RefreshControlExample.js create mode 100644 Libraries/Components/RefreshControl/RefreshControl.js create mode 100644 React/Views/RCTRefreshControl.h create mode 100644 React/Views/RCTRefreshControl.m create mode 100644 React/Views/RCTRefreshControlManager.h create mode 100644 React/Views/RCTRefreshControlManager.m diff --git a/Examples/UIExplorer/RefreshControlExample.js b/Examples/UIExplorer/RefreshControlExample.js new file mode 100644 index 0000000000..b8feefb661 --- /dev/null +++ b/Examples/UIExplorer/RefreshControlExample.js @@ -0,0 +1,125 @@ +/** +* 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. +* +*/ +'use strict'; + +const React = require('react-native'); +const { + ScrollView, + StyleSheet, + RefreshControl, + Text, + TouchableWithoutFeedback, + View, +} = React; + +const styles = StyleSheet.create({ + row: { + borderColor: 'grey', + borderWidth: 1, + padding: 20, + backgroundColor: '#3a5795', + margin: 5, + }, + text: { + alignSelf: 'center', + color: '#fff', + }, + scrollview: { + flex: 1, + }, +}); + +const Row = React.createClass({ + _onClick: function() { + this.props.onClick(this.props.data); + }, + render: function() { + return ( + + + + {this.props.data.text + ' (' + this.props.data.clicks + ' clicks)'} + + + + ); + }, +}); + +const RefreshControlExample = React.createClass({ + statics: { + title: '', + description: 'Adds pull-to-refresh support to a scrollview.' + }, + + getInitialState() { + return { + isRefreshing: false, + loaded: 0, + rowData: Array.from(new Array(20)).map( + (val, i) => ({text: 'Initial row' + i, clicks: 0})), + }; + }, + + _onClick(row) { + row.clicks++; + this.setState({ + rowData: this.state.rowData, + }); + }, + + render() { + const rows = this.state.rowData.map((row, ii) => { + return ; + }); + return ( + + }> + {rows} + + ); + }, + + _onRefresh() { + this.setState({isRefreshing: true}); + setTimeout(() => { + // prepend 10 items + const rowData = Array.from(new Array(10)) + .map((val, i) => ({ + text: 'Loaded row' + (+this.state.loaded + i), + clicks: 0, + })) + .concat(this.state.rowData); + + this.setState({ + loaded: this.state.loaded + 10, + isRefreshing: false, + rowData: rowData, + }); + }, 5000); + }, +}); + +module.exports = RefreshControlExample; diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 7aa70c5163..81442e0b98 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -28,6 +28,7 @@ var COMPONENTS = [ require('./ProgressBarAndroidExample'), require('./ScrollViewSimpleExample'), require('./SwitchAndroidExample'), + require('./RefreshControlExample'), require('./PullToRefreshViewAndroidExample.android'), require('./TextExample.android'), require('./TextInputExample.android'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index c2ba817520..1d96b2d49c 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -42,6 +42,7 @@ var COMPONENTS = [ require('./NavigatorIOSExample'), require('./PickerIOSExample'), require('./ProgressViewIOSExample'), + require('./RefreshControlExample'), require('./ScrollViewExample'), require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), diff --git a/Libraries/Components/RefreshControl/RefreshControl.js b/Libraries/Components/RefreshControl/RefreshControl.js new file mode 100644 index 0000000000..319f054c38 --- /dev/null +++ b/Libraries/Components/RefreshControl/RefreshControl.js @@ -0,0 +1,96 @@ +/** + * 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 RefreshControl + */ +'use strict'; + +const React = require('React'); +const Platform = require('Platform'); +const ColorPropType = require('ColorPropType'); + +const requireNativeComponent = require('requireNativeComponent'); + +if (Platform.OS === 'ios') { + var RefreshLayoutConsts = {SIZE: {}}; +} else if (Platform.OS === 'android') { + var RefreshLayoutConsts = require('NativeModules').UIManager.AndroidSwipeRefreshLayout.Constants; +} + +/** + * This component is used inside a ScrollView to add pull to refresh + * functionality. When the ScrollView is at `scrollY: 0`, swiping down + * triggers an `onRefresh` event. + */ +const RefreshControl = React.createClass({ + statics: { + SIZE: RefreshLayoutConsts.SIZE, + }, + + propTypes: { + /** + * Called when the view starts refreshing. + */ + onRefresh: React.PropTypes.func, + /** + * Whether the view should be indicating an active refresh. + */ + refreshing: React.PropTypes.bool, + /** + * The color of the refresh indicator. + * @platform ios + */ + tintColor: ColorPropType, + /** + * The title displayed under the refresh indicator. + * @platform ios + */ + title: React.PropTypes.string, + /** + * Whether the pull to refresh functionality is enabled. + * @platform android + */ + enabled: React.PropTypes.bool, + /** + * The colors (at least one) that will be used to draw the refresh indicator. + * @platform android + */ + colors: React.PropTypes.arrayOf(ColorPropType), + /** + * The background color of the refresh indicator. + * @platform android + */ + progressBackgroundColor: ColorPropType, + /** + * Size of the refresh indicator, see RefreshControl.SIZE. + * @platform android + */ + size: React.PropTypes.oneOf(RefreshLayoutConsts.SIZE.DEFAULT, RefreshLayoutConsts.SIZE.LARGE), + }, + + render() { + if (Platform.OS === 'ios') { + return ; + } else { + // On Android the ScrollView is wrapped so this component doesn't render + // anything and only acts as a way to configure the wrapper view. + // ScrollView will wrap itself in a AndroidSwipeRefreshLayout using props + // from this. + return null; + } + }, +}); + +if (Platform.OS === 'ios') { + var NativeRefreshControl = requireNativeComponent( + 'RCTRefreshControl', + RefreshControl + ); +} + +module.exports = RefreshControl; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 870044ecaa..a5d87e082a 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -31,6 +31,7 @@ var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); var pointsDiffer = require('pointsDiffer'); var requireNativeComponent = require('requireNativeComponent'); +var processColor = require('processColor'); var PropTypes = React.PropTypes; @@ -294,19 +295,16 @@ var ScrollView = React.createClass({ zoomScale: PropTypes.number, /** - * When defined, displays a UIRefreshControl. - * Invoked with a function to stop refreshing when the UIRefreshControl is animating. - * - * ``` - * (endRefreshing) => { - * endRefreshing(); - * } - * ``` - * + * A RefreshControl component, used to provide pull-to-refresh + * functionality for the ScrollView. + */ + refreshControl: PropTypes.element, + + /** + * Deprecated - use `refreshControl` property instead. * @platform ios */ onRefreshStart: PropTypes.func, - }, mixins: [ScrollResponder.Mixin], @@ -445,11 +443,13 @@ var ScrollView = React.createClass({ }; var onRefreshStart = this.props.onRefreshStart; - // this is necessary because if we set it on props, even when empty, - // it'll trigger the default pull-to-refresh behavior on native. - props.onRefreshStart = onRefreshStart - ? function() { onRefreshStart && onRefreshStart(this.endRefreshing); }.bind(this) - : null; + if (onRefreshStart) { + console.warn('onRefreshStart is deprecated. Use the refreshControl prop instead.'); + // this is necessary because if we set it on props, even when empty, + // it'll trigger the default pull-to-refresh behavior on native. + props.onRefreshStart = + function() { onRefreshStart && onRefreshStart(this.endRefreshing); }.bind(this); + } var ScrollViewClass; if (Platform.OS === 'ios') { @@ -466,6 +466,33 @@ var ScrollView = React.createClass({ 'ScrollViewClass must not be undefined' ); + var refreshControl = this.props.refreshControl; + if (refreshControl) { + if (Platform.OS === 'ios') { + // On iOS the RefreshControl is a child of the ScrollView. + return ( + + {refreshControl} + {contentContainer} + + ); + } else if (Platform.OS === 'android') { + // On Android wrap the ScrollView with a AndroidSwipeRefreshLayout. + // Since the ScrollView is wrapped add the style props to the + // AndroidSwipeRefreshLayout and use flex: 1 for the ScrollView. + var refreshProps = refreshControl.props; + return ( + + + {contentContainer} + + + ); + } + } return ( {contentContainer} @@ -519,6 +546,7 @@ if (Platform.OS === 'android') { 'AndroidHorizontalScrollView', ScrollView ); + var AndroidSwipeRefreshLayout = requireNativeComponent('AndroidSwipeRefreshLayout'); } else if (Platform.OS === 'ios') { var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView); } diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 5b33bd3634..b28f255d9c 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -35,6 +35,7 @@ var ReactNative = { get Switch() { return require('Switch'); }, get PullToRefreshViewAndroid() { return require('PullToRefreshViewAndroid'); }, get RecyclerViewBackedScrollView() { return require('RecyclerViewBackedScrollView'); }, + get RefreshControl() { return require('RefreshControl'); }, get SwitchAndroid() { return require('SwitchAndroid'); }, get SwitchIOS() { return require('SwitchIOS'); }, get TabBarIOS() { return require('TabBarIOS'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index a250e57b0e..7773b27c02 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -10,7 +10,7 @@ * and Flow doesn't have a good way to enable getters and setters for * react-native without forcing all react-native users to also enable getters * and setters. Until we solve that issue, we can use this .flow file to - * pretend like react-native doesn't use getters and setters + * pretend like react-native doesn't use getters and setters * * @flow */ @@ -47,6 +47,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { Switch: require('Switch'), PullToRefreshViewAndroid: require('PullToRefreshViewAndroid'), RecyclerViewBackedScrollView: require('RecyclerViewBackedScrollView'), + RefreshControl: require('RefreshControl'), SwitchAndroid: require('SwitchAndroid'), SwitchIOS: require('SwitchIOS'), TabBarIOS: require('TabBarIOS'), diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 141bed35aa..a1802c9444 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -71,6 +71,8 @@ 14F7A0EC1BDA3B3C003C6C10 /* RCTPerfMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F7A0EB1BDA3B3C003C6C10 /* RCTPerfMonitor.m */; }; 14F7A0F01BDA714B003C6C10 /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F7A0EF1BDA714B003C6C10 /* RCTFPSGraph.m */; }; 1BCBD4A71C32FA0B006FC476 /* RCTBundleURLProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCBD4A61C32FA0B006FC476 /* RCTBundleURLProcessor.m */; }; + 191E3EBE1C29D9AF00C180A6 /* RCTRefreshControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 191E3EBD1C29D9AF00C180A6 /* RCTRefreshControlManager.m */; }; + 191E3EC11C29DC3800C180A6 /* RCTRefreshControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 191E3EC01C29DC3800C180A6 /* RCTRefreshControl.m */; }; 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A131AAE854800E7D092 /* RCTPicker.m */; }; 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; @@ -245,6 +247,10 @@ 14F7A0EF1BDA714B003C6C10 /* RCTFPSGraph.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFPSGraph.m; sourceTree = ""; }; 1BCBD4A51C32FA0B006FC476 /* RCTBundleURLProcessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBundleURLProcessor.h; sourceTree = ""; }; 1BCBD4A61C32FA0B006FC476 /* RCTBundleURLProcessor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBundleURLProcessor.m; sourceTree = ""; }; + 191E3EBC1C29D9AF00C180A6 /* RCTRefreshControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRefreshControlManager.h; sourceTree = ""; }; + 191E3EBD1C29D9AF00C180A6 /* RCTRefreshControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRefreshControlManager.m; sourceTree = ""; }; + 191E3EBF1C29DC3800C180A6 /* RCTRefreshControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRefreshControl.h; sourceTree = ""; }; + 191E3EC01C29DC3800C180A6 /* RCTRefreshControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRefreshControl.m; sourceTree = ""; }; 58114A121AAE854800E7D092 /* RCTPicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPicker.h; sourceTree = ""; }; 58114A131AAE854800E7D092 /* RCTPicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPicker.m; sourceTree = ""; }; 58114A141AAE854800E7D092 /* RCTPickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPickerManager.h; sourceTree = ""; }; @@ -401,6 +407,10 @@ 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, 13513F3A1B1F43F400FCE529 /* RCTProgressViewManager.h */, 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */, + 191E3EBF1C29DC3800C180A6 /* RCTRefreshControl.h */, + 191E3EC01C29DC3800C180A6 /* RCTRefreshControl.m */, + 191E3EBC1C29D9AF00C180A6 /* RCTRefreshControlManager.h */, + 191E3EBD1C29D9AF00C180A6 /* RCTRefreshControlManager.m */, 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */, 13B07FF61A6947C200A75B9A /* RCTScrollView.h */, 13B07FF71A6947C200A75B9A /* RCTScrollView.m */, @@ -703,6 +713,7 @@ 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */, + 191E3EC11C29DC3800C180A6 /* RCTRefreshControl.m in Sources */, 13C156051AB1A2840079392D /* RCTWebView.m in Sources */, 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */, 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */, @@ -713,6 +724,7 @@ 1450FF871BCFF28A00208362 /* RCTProfileTrampoline-arm.S in Sources */, 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, + 191E3EBE1C29D9AF00C180A6 /* RCTRefreshControlManager.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */, diff --git a/React/Views/RCTRefreshControl.h b/React/Views/RCTRefreshControl.h new file mode 100644 index 0000000000..4316576aab --- /dev/null +++ b/React/Views/RCTRefreshControl.h @@ -0,0 +1,19 @@ +/** + * 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 + +#import "RCTComponent.h" + +@interface RCTRefreshControl : UIRefreshControl + +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) RCTDirectEventBlock onRefresh; + +@end diff --git a/React/Views/RCTRefreshControl.m b/React/Views/RCTRefreshControl.m new file mode 100644 index 0000000000..d3fc0e7f9b --- /dev/null +++ b/React/Views/RCTRefreshControl.m @@ -0,0 +1,54 @@ +/** + * 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 "RCTRefreshControl.h" + +#import "RCTUtils.h" + +@implementation RCTRefreshControl + +- (instancetype)init +{ + if ((self = [super init])) { + [self addTarget:self action:@selector(refreshControlValueChanged) forControlEvents:UIControlEventValueChanged]; + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) + +- (NSString *)title +{ + return self.attributedTitle.string; +} + +- (void)setTitle:(NSString *)title +{ + self.attributedTitle = [[NSAttributedString alloc] initWithString:title]; +} + +- (void)setRefreshing:(BOOL)refreshing +{ + if (super.refreshing != refreshing) { + if (refreshing) { + [self beginRefreshing]; + } else { + [self endRefreshing]; + } + } +} + +- (void)refreshControlValueChanged +{ + if (_onRefresh) { + _onRefresh(nil); + } +} + +@end diff --git a/React/Views/RCTRefreshControlManager.h b/React/Views/RCTRefreshControlManager.h new file mode 100644 index 0000000000..8d1c3f9616 --- /dev/null +++ b/React/Views/RCTRefreshControlManager.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 RCTRefreshControlManager : RCTViewManager + +@end diff --git a/React/Views/RCTRefreshControlManager.m b/React/Views/RCTRefreshControlManager.m new file mode 100644 index 0000000000..bc479a150e --- /dev/null +++ b/React/Views/RCTRefreshControlManager.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 "RCTRefreshControlManager.h" + +#import "RCTRefreshControl.h" + +@implementation RCTRefreshControlManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [RCTRefreshControl new]; +} + +RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(title, NSString) + +@end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 5ae7fb05a6..de9bcb273d 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -14,6 +14,7 @@ #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" +#import "RCTRefreshControl.h" #import "RCTUIManager.h" #import "RCTUtils.h" #import "UIView+Private.h" @@ -410,20 +411,31 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex { - RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview"); - _contentView = view; - [_scrollView addSubview:view]; + if ([view isKindOfClass:[RCTRefreshControl class]]) { + _scrollView.refreshControl = (RCTRefreshControl*)view; + } else { + RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview"); + _contentView = view; + [_scrollView addSubview:view]; + } } - (void)removeReactSubview:(UIView *)subview { - RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); - _contentView = nil; - [subview removeFromSuperview]; + if ([subview isKindOfClass:[RCTRefreshControl class]]) { + _scrollView.refreshControl = nil; + } else { + RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); + _contentView = nil; + [subview removeFromSuperview]; + } } - (NSArray *)reactSubviews { + if (_contentView && _scrollView.refreshControl) { + return @[_contentView, _scrollView.refreshControl]; + } return _contentView ? @[_contentView] : @[]; }