From 93bbc6482d8132def654ba6f32e1a1ac01a2d69c Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Wed, 22 Jul 2015 15:09:43 -0700 Subject: [PATCH 1/9] Unbreak the dismissing keyboard behavior on Android --- Libraries/Components/ScrollResponder.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 168cb36fa60b50..ab03e55fbbc6f1 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -470,7 +470,11 @@ var ScrollResponderMixin = { }, scrollResponderKeyboardDidShow: function(e: Event) { - this.keyboardWillOpenTo = null; + // TODO(7693961): The event for DidShow is not available on iOS yet. + // Use the one from WillShow and do not assign. + if (e) { + this.keyboardWillOpenTo = e; + } this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e); }, From e06af51cf940c8eeaf7166e80dc3a4a610a0108a Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 22 Jul 2015 16:36:46 -0700 Subject: [PATCH 2/9] [react-native] inspector + devtools, naming things --- Libraries/Inspector/ElementProperties.js | 6 +++- Libraries/Inspector/Inspector.js | 37 ++++++++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Libraries/Inspector/ElementProperties.js b/Libraries/Inspector/ElementProperties.js index 8554238453d2c8..222da109fec82f 100644 --- a/Libraries/Inspector/ElementProperties.js +++ b/Libraries/Inspector/ElementProperties.js @@ -27,7 +27,11 @@ var mapWithSeparator = require('mapWithSeparator'); var ElementProperties = React.createClass({ propTypes: { hierarchy: PropTypes.array.isRequired, - style: PropTypes.array.isRequired, + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array, + PropTypes.number, + ]), }, render: function() { diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 6015bcf3ff2d97..c014c29c43de10 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -20,7 +20,7 @@ var StyleSheet = require('StyleSheet'); var UIManager = require('NativeModules').UIManager; var View = require('View'); -var REACT_DEVTOOLS_HOOK: ?Object = typeof window !== 'undefined' ? window.__REACT_DEVTOOLS_BACKEND__ : null; +var REACT_DEVTOOLS_HOOK: ?Object = typeof window !== 'undefined' ? window.__REACT_DEVTOOLS_GLOBAL_HOOK__ : null; if (REACT_DEVTOOLS_HOOK) { // required for devtools to be able to edit react native styles @@ -34,7 +34,7 @@ class Inspector extends React.Component { super(props); this.state = { - devtoolsBackend: null, + devtoolsAgent: null, panelPos: 'bottom', inspecting: true, perfing: false, @@ -45,14 +45,10 @@ class Inspector extends React.Component { componentDidMount() { if (REACT_DEVTOOLS_HOOK) { this.attachToDevtools = this.attachToDevtools.bind(this); - REACT_DEVTOOLS_HOOK.addStartupListener(this.attachToDevtools); + REACT_DEVTOOLS_HOOK.on('react-devtools', this.attachToDevtools); // if devtools is already started - // TODO(jared): should addStartupListener just go ahead and call the - // listener if the devtools is already started? might be unexpected... - // is there some name other than `addStartupListener` that would be - // better? - if (REACT_DEVTOOLS_HOOK.backend) { - this.attachToDevtools(REACT_DEVTOOLS_HOOK.backend); + if (REACT_DEVTOOLS_HOOK.reactDevtoolsAgent) { + this.attachToDevtools(REACT_DEVTOOLS_HOOK.reactDevtoolsAgent); } } } @@ -62,13 +58,13 @@ class Inspector extends React.Component { this._subs.map(fn => fn()); } if (REACT_DEVTOOLS_HOOK) { - REACT_DEVTOOLS_HOOK.removeStartupListener(this.attachToDevtools); + REACT_DEVTOOLS_HOOK.off('react-devtools', this.attachToDevtools); } } - attachToDevtools(backend: Object) { + attachToDevtools(agent: Object) { var _hideWait = null; - var hlSub = backend.sub('highlight', ({node, name, props}) => { + var hlSub = agent.sub('highlight', ({node, name, props}) => { clearTimeout(_hideWait); UIManager.measure(node, (x, y, width, height, left, top) => { this.setState({ @@ -80,7 +76,10 @@ class Inspector extends React.Component { }); }); }); - var hideSub = backend.sub('hideHighlight', () => { + var hideSub = agent.sub('hideHighlight', () => { + if (this.state.inspected === null) { + return; + } // we wait to actually hide in order to avoid flicker _hideWait = setTimeout(() => { this.setState({ @@ -90,12 +89,12 @@ class Inspector extends React.Component { }); this._subs = [hlSub, hideSub]; - backend.on('shutdown', () => { - this.setState({devtoolsBackend: null}); + agent.on('shutdown', () => { + this.setState({devtoolsAgent: null}); this._subs = null; }); this.setState({ - devtoolsBackend: backend, + devtoolsAgent: agent, }); } @@ -114,8 +113,8 @@ class Inspector extends React.Component { } onTouchInstance(instance: Object, frame: Object, pointerY: number) { - if (this.state.devtoolsBackend) { - this.state.devtoolsBackend.selectFromReactInstance(instance, true); + if (this.state.devtoolsAgent) { + this.state.devtoolsAgent.selectFromReactInstance(instance, true); } var hierarchy = InspectorUtils.getOwnerHierarchy(instance); var publicInstance = instance.getPublicInstance(); @@ -159,7 +158,7 @@ class Inspector extends React.Component { />} Date: Wed, 22 Jul 2015 17:09:18 -0700 Subject: [PATCH 3/9] [ReactNative][BREAKING_CHANGE] Remove cloneElement from TouchableBounce --- Examples/2048/Game2048.js | 6 +- .../Components/Touchable/TouchableBounce.js | 81 +++++++------------ 2 files changed, 32 insertions(+), 55 deletions(-) diff --git a/Examples/2048/Game2048.js b/Examples/2048/Game2048.js index 9fd6052c968e1b..e43eefa758da22 100644 --- a/Examples/2048/Game2048.js +++ b/Examples/2048/Game2048.js @@ -136,10 +136,8 @@ class GameEndOverlay extends React.Component { return ( {message} - - - Try Again? - + + Try Again? ); diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 9aad783b06595f..bada73041f274d 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -11,20 +11,12 @@ */ 'use strict'; -var AnimationExperimental = require('AnimationExperimental'); +var Animated = require('Animated'); var NativeMethodsMixin = require('NativeMethodsMixin'); -var POPAnimation = require('POPAnimation'); var React = require('React'); var Touchable = require('Touchable'); var merge = require('merge'); -var onlyChild = require('onlyChild'); - -var invariant = require('invariant'); -invariant( - AnimationExperimental || POPAnimation, - 'Please add the RCTAnimationExperimental framework to your project, or add //Libraries/FBReactKit:RCTPOPAnimation to your BUCK file if running internally within Facebook.' -); type State = { animationID: ?number; @@ -58,40 +50,23 @@ var TouchableBounce = React.createClass({ }, getInitialState: function(): State { - return merge(this.touchableGetInitialState(), {animationID: null}); + return { + ...this.touchableGetInitialState(), + scale: new Animated.Value(1), + }; }, bounceTo: function( value: number, velocity: number, bounciness: number, - fromValue?: ?number, callback?: ?Function ) { - if (POPAnimation) { - this.state.animationID && this.removeAnimation(this.state.animationID); - var anim = { - property: POPAnimation.Properties.scaleXY, - dynamicsTension: 0, - toValue: [value, value], - velocity: [velocity, velocity], - springBounciness: bounciness, - fromValue: fromValue ? [fromValue, fromValue] : undefined, - }; - this.state.animationID = POPAnimation.createSpringAnimation(anim); - this.addAnimation(this.state.animationID, callback); - } else { - AnimationExperimental.startAnimation( - { - node: this, - duration: 300, - easing: 'easeOutBack', - property: 'scaleXY', - toValue: { x: value, y: value}, - }, - callback - ); - } + Animated.spring(this.state.scale, { + toValue: value, + velocity, + bounciness, + }).start(callback); }, /** @@ -109,13 +84,14 @@ var TouchableBounce = React.createClass({ touchableHandlePress: function() { var onPressWithCompletion = this.props.onPressWithCompletion; if (onPressWithCompletion) { - onPressWithCompletion( - this.bounceTo.bind(this, 1, 10, 10, 0.93, this.props.onPressAnimationComplete) - ); + onPressWithCompletion(() => { + this.state.scale.setValue(0.93); + this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete); + }); return; } - this.bounceTo(1, 10, 10, undefined, this.props.onPressAnimationComplete); + this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete); this.props.onPress && this.props.onPress(); }, @@ -127,18 +103,21 @@ var TouchableBounce = React.createClass({ return 0; }, - render: function() { - var child = onlyChild(this.props.children); - return React.cloneElement(child, { - accessible: true, - testID: this.props.testID, - onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, - onResponderGrant: this.touchableHandleResponderGrant, - onResponderMove: this.touchableHandleResponderMove, - onResponderRelease: this.touchableHandleResponderRelease, - onResponderTerminate: this.touchableHandleResponderTerminate - }); + render: function(): ReactElement { + return ( + + {this.props.children} + + ); } }); From fea2db42fdbbb4e9cf6420f77048c7dab47b4ef0 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Wed, 22 Jul 2015 17:20:27 -0700 Subject: [PATCH 4/9] [Animated] Send a final update with toValue for spring Summary: Animated.spring is not guarantee to stabilize at exactly toValue (determined by restDisplacementThreshold). It is a bit annoying that the last value is not toValue, it makes the logs harder to read and also prevents you from writing code like value === toValue. Instead you need to track it down somewhere else. --- Libraries/Animation/Animated/Animated.js | 6 ++++++ .../Animation/Animated/__tests__/Animated-test.js | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/Libraries/Animation/Animated/Animated.js b/Libraries/Animation/Animated/Animated.js index f99913f505ba63..8f3f383984a39d 100644 --- a/Libraries/Animation/Animated/Animated.js +++ b/Libraries/Animation/Animated/Animated.js @@ -479,7 +479,13 @@ class SpringAnimation extends Animation { if (this._tension !== 0) { isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold; } + if (isOvershooting || (isVelocity && isDisplacement)) { + if (this._tension !== 0) { + // Ensure that we end up with a round value + this._onUpdate(this._toValue); + } + this.__debouncedOnEnd({finished: true}); return; } diff --git a/Libraries/Animation/Animated/__tests__/Animated-test.js b/Libraries/Animation/Animated/__tests__/Animated-test.js index cad752ff016cc7..d27ff920c14891 100644 --- a/Libraries/Animation/Animated/__tests__/Animated-test.js +++ b/Libraries/Animation/Animated/__tests__/Animated-test.js @@ -127,6 +127,19 @@ describe('Animated', () => { Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback); expect(callback).toBeCalled(); }); + + it('send toValue when a spring stops', () => { + var anim = new Animated.Value(0); + var listener = jest.genMockFunction(); + anim.addListener(listener); + Animated.spring(anim, {toValue: 15}).start(); + jest.runAllTimers(); + var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value; + expect(lastValue).not.toBe(15); + expect(lastValue).toBeCloseTo(15); + expect(anim.__getValue()).toBe(15); + }); + }); From 6fea7c2846d3977e2b6549aa3243d582b4c39c78 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Wed, 22 Jul 2015 18:09:55 -0700 Subject: [PATCH 5/9] [ReactNative] Remove all the flow errors --- Libraries/react-native/react-native-interface.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Libraries/react-native/react-native-interface.js b/Libraries/react-native/react-native-interface.js index b76518387826eb..623ffd98c93ae2 100644 --- a/Libraries/react-native/react-native-interface.js +++ b/Libraries/react-native/react-native-interface.js @@ -21,3 +21,6 @@ declare var fetch: any; declare var Headers: any; declare var Request: any; declare var Response: any; +declare module requestAnimationFrame { + declare var exports: (callback: any) => any; +} From 9d19829742dc2313009de451b5b198df526e79d4 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 23 Jul 2015 03:55:12 -0700 Subject: [PATCH 6/9] Refactored networking logic out into RCTDownloadTask --- Libraries/Network/RCTDownloadTask.h | 40 ++ Libraries/Network/RCTDownloadTask.m | 102 +++++ Libraries/Network/RCTHTTPRequestHandler.h | 3 + Libraries/Network/RCTHTTPRequestHandler.m | 12 +- .../RCTNetwork.xcodeproj/project.pbxproj | 6 + Libraries/Network/RCTNetworking.h | 1 - Libraries/Network/RCTNetworking.m | 397 ++++++------------ Libraries/Network/XMLHttpRequest.ios.js | 2 +- React/Base/RCTURLRequestDelegate.h | 6 +- 9 files changed, 299 insertions(+), 270 deletions(-) create mode 100644 Libraries/Network/RCTDownloadTask.h create mode 100644 Libraries/Network/RCTDownloadTask.m diff --git a/Libraries/Network/RCTDownloadTask.h b/Libraries/Network/RCTDownloadTask.h new file mode 100644 index 00000000000000..fe434c75834934 --- /dev/null +++ b/Libraries/Network/RCTDownloadTask.h @@ -0,0 +1,40 @@ +/** + * 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 "RCTURLRequestDelegate.h" +#import "RCTURLRequestHandler.h" + +typedef void (^RCTURLRequestCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error); +typedef void (^RCTURLRequestCancellationBlock)(void); +typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data); +typedef void (^RCTURLRequestProgressBlock)(double progress, double total); +typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response); + +@interface RCTDownloadTask : NSObject + +@property (nonatomic, readonly) NSURLRequest *request; +@property (nonatomic, readonly) NSNumber *requestID; +@property (nonatomic, readonly) id requestToken; +@property (nonatomic, readonly) NSURLResponse *response; +@property (nonatomic, readonly) RCTURLRequestCompletionBlock completionBlock; + +@property (nonatomic, copy) RCTURLRequestProgressBlock downloadProgressBlock; +@property (nonatomic, copy) RCTURLRequestIncrementalDataBlock incrementalDataBlock; +@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock; +@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock; + +- (instancetype)initWithRequest:(NSURLRequest *)request + handler:(id)handler + completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER; + +- (void)cancel; + +@end diff --git a/Libraries/Network/RCTDownloadTask.m b/Libraries/Network/RCTDownloadTask.m new file mode 100644 index 00000000000000..e0b86dacba826c --- /dev/null +++ b/Libraries/Network/RCTDownloadTask.m @@ -0,0 +1,102 @@ +/** + * 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 "RCTDownloadTask.h" + +#import "RCTAssert.h" + +@implementation RCTDownloadTask +{ + NSMutableData *_data; + id _handler; + RCTDownloadTask *_selfReference; +} + +- (instancetype)initWithRequest:(NSURLRequest *)request + handler:(id)handler + completionBlock:(RCTURLRequestCompletionBlock)completionBlock +{ + RCTAssertParam(request); + RCTAssertParam(handler); + RCTAssertParam(completionBlock); + + static NSUInteger requestID = 0; + + if ((self = [super init])) { + if (!(_requestToken = [handler sendRequest:request withDelegate:self])) { + return nil; + } + _requestID = @(requestID++); + _request = request; + _handler = handler; + _completionBlock = completionBlock; + _selfReference = self; + } + return self; +} + +- (void)invalidate +{ + _selfReference = nil; + _completionBlock = nil; + _downloadProgressBlock = nil; + _incrementalDataBlock = nil; + _responseBlock = nil; + _uploadProgressBlock = nil; +} + +RCT_NOT_IMPLEMENTED(-init) + +- (void)cancel +{ + if ([_handler respondsToSelector:@selector(cancelRequest:)]) { + [_handler cancelRequest:_requestToken]; + } + [self invalidate]; +} + +- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent +{ + RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken); + if (_uploadProgressBlock) { + _uploadProgressBlock(bytesSent, _request.HTTPBody.length); + } +} + +- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response +{ + RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken); + _response = response; + if (_responseBlock) { + _responseBlock(response); + } +} + +- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data +{ + RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken); + if (!_data) { + _data = [[NSMutableData alloc] init]; + } + [_data appendData:data]; + if (_incrementalDataBlock) { + _incrementalDataBlock(data); + } + if (_downloadProgressBlock && _response.expectedContentLength > 0) { + _downloadProgressBlock(_data.length, _response.expectedContentLength); + } +} + +- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error +{ + _completionBlock(_response, _data, error); + [self invalidate]; +} + +@end diff --git a/Libraries/Network/RCTHTTPRequestHandler.h b/Libraries/Network/RCTHTTPRequestHandler.h index b8a7a3e26e50f3..155491e6310873 100644 --- a/Libraries/Network/RCTHTTPRequestHandler.h +++ b/Libraries/Network/RCTHTTPRequestHandler.h @@ -10,6 +10,9 @@ #import "RCTURLRequestHandler.h" #import "RCTInvalidating.h" +/** + * This is the default RCTURLRequestHandler implementation for HTTP requests. + */ @interface RCTHTTPRequestHandler : NSObject @end diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m index c89a4fbc9c46b8..d5ee89a459210b 100644 --- a/Libraries/Network/RCTHTTPRequestHandler.m +++ b/Libraries/Network/RCTHTTPRequestHandler.m @@ -50,8 +50,8 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request return [@[@"http", @"https", @"file"] containsObject:[request.URL.scheme lowercaseString]]; } -- (id)sendRequest:(NSURLRequest *)request - withDelegate:(id)delegate +- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request + withDelegate:(id)delegate { // Lazy setup if (!_session && [self isValid]) { @@ -68,9 +68,10 @@ - (id)sendRequest:(NSURLRequest *)request return task; } -- (void)cancelRequest:(NSURLSessionDataTask *)requestToken +- (void)cancelRequest:(NSURLSessionDataTask *)task { - [requestToken cancel]; + [task cancel]; + [_delegates removeObjectForKey:task]; } #pragma mark - NSURLSession delegate @@ -81,10 +82,9 @@ - (void)URLSession:(NSURLSession *)session totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { - [[_delegates objectForKey:task] URLRequest:task didUploadProgress:(double)totalBytesSent total:(double)totalBytesExpectedToSend]; + [[_delegates objectForKey:task] URLRequest:task didSendDataWithProgress:totalBytesSent]; } - - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task didReceiveResponse:(NSURLResponse *)response diff --git a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj index 2a32628710b2d5..88835214ba8167 100644 --- a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj +++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; }; + 13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */; }; 352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */; }; 58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTNetworking.m */; }; /* End PBXBuildFile section */ @@ -27,6 +28,8 @@ /* Begin PBXFileReference section */ 1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = ""; }; 1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = ""; }; + 13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTask.h; sourceTree = ""; }; + 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTask.m; sourceTree = ""; }; 352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPRequestHandler.h; sourceTree = ""; }; 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHTTPRequestHandler.m; sourceTree = ""; }; 58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -48,6 +51,8 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */, + 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */, 352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */, 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */, 58B512061A9E6CE300147676 /* RCTNetworking.h */, @@ -124,6 +129,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */, 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */, 58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */, 352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */, diff --git a/Libraries/Network/RCTNetworking.h b/Libraries/Network/RCTNetworking.h index d8bc3952483fec..3e54354b5ecc78 100644 --- a/Libraries/Network/RCTNetworking.h +++ b/Libraries/Network/RCTNetworking.h @@ -14,4 +14,3 @@ @interface RCTNetworking : NSObject @end - diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index d73f1fb4dadc24..0af33fe78d5981 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -11,18 +11,19 @@ #import "RCTAssert.h" #import "RCTConvert.h" +#import "RCTDownloadTask.h" #import "RCTURLRequestHandler.h" #import "RCTEventDispatcher.h" #import "RCTHTTPRequestHandler.h" #import "RCTLog.h" #import "RCTUtils.h" -typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result); +typedef RCTURLRequestCancellationBlock (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result); -@interface RCTNetworking () - -- (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback; +@interface RCTNetworking () +- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)data + callback:(RCTHTTPQueryResult)callback; @end /** @@ -30,7 +31,7 @@ - (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError */ @interface RCTHTTPFormDataHelper : NSObject -@property (nonatomic, weak) RCTNetworking *dataManager; +@property (nonatomic, weak) RCTNetworking *networker; @end @@ -42,51 +43,49 @@ @implementation RCTHTTPFormDataHelper NSString *boundary; } -- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))callback +static NSString *RCTGenerateFormBoundary() { - if (![formData count]) { - callback(nil, nil); - return; + const size_t boundaryLength = 70; + const char *boundaryChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./"; + + char *bytes = malloc(boundaryLength); + size_t charCount = strlen(boundaryChars); + for (int i = 0; i < boundaryLength; i++) { + bytes[i] = boundaryChars[arc4random_uniform((u_int32_t)charCount)]; + } + return [[NSString alloc] initWithBytesNoCopy:bytes length:boundaryLength encoding:NSUTF8StringEncoding freeWhenDone:YES]; +} + +- (RCTURLRequestCancellationBlock)process:(NSArray *)formData + callback:(RCTHTTPQueryResult)callback +{ + if (formData.count == 0) { + return callback(nil, nil); } + parts = [formData mutableCopy]; _callback = callback; multipartBody = [[NSMutableData alloc] init]; - boundary = [self generateBoundary]; + boundary = RCTGenerateFormBoundary(); - NSDictionary *currentPart = [parts objectAtIndex: 0]; - [_dataManager processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) { - [self handleResult:r error:e]; + return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *error, NSDictionary *result) { + return [self handleResult:result error:error]; }]; } -- (NSString *)generateBoundary -{ - NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./"; - const NSUInteger boundaryLength = 70; - - NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength]; - NSUInteger numchars = [boundaryChars length]; - for (NSUInteger i = 0; i < boundaryLength; i++) { - [output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]]; - } - return output; -} - -- (void)handleResult:(NSDictionary *)result error:(NSError *)error +- (RCTURLRequestCancellationBlock)handleResult:(NSDictionary *)result + error:(NSError *)error { if (error) { - _callback(error, nil); - return; + return _callback(error, nil); } - NSDictionary *currentPart = parts[0]; - [parts removeObjectAtIndex:0]; // Start with boundary. [multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; // Print headers. - NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy]; + NSMutableDictionary *headers = [parts[0][@"headers"] mutableCopy]; NSString *partContentType = result[@"contentType"]; if (partContentType != nil) { [headers setObject:partContentType forKey:@"content-type"]; @@ -101,110 +100,18 @@ - (void)handleResult:(NSDictionary *)result error:(NSError *)error [multipartBody appendData:result[@"body"]]; [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; - if ([parts count]) { - NSDictionary *nextPart = [parts objectAtIndex: 0]; - [_dataManager processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) { - [self handleResult:r error:e]; + [parts removeObjectAtIndex:0]; + if (parts.count) { + return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *err, NSDictionary *res) { + return [self handleResult:res error:err]; }]; - return; } // We've processed the last item. Finish and return. [multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary]; - _callback(nil, @{@"body": multipartBody, @"contentType": contentType}); -} - -@end - -/** - * Helper to package in-flight requests together with their response data. - */ -@interface RCTActiveURLRequest : NSObject - -@property (nonatomic, strong) NSNumber *requestID; -@property (nonatomic, strong) NSURLRequest *request; -@property (nonatomic, strong) id handler; -@property (nonatomic, assign) BOOL incrementalUpdates; -@property (nonatomic, strong) NSURLResponse *response; -@property (nonatomic, strong) NSMutableData *data; - -@end - -@implementation RCTActiveURLRequest - -- (instancetype)init -{ - if ((self = [super init])) { - _data = [[NSMutableData alloc] init]; - } - return self; -} - -@end - -/** - * Helper to load request body data using a handler. - */ -@interface RCTDataLoader : NSObject - -@end - -typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError *error); - -@implementation RCTDataLoader -{ - RCTDataLoaderCallback _callback; - RCTActiveURLRequest *_request; - id _requestToken; -} - -- (instancetype)initWithRequest:(NSURLRequest *)request - handler:(id)handler - callback:(RCTDataLoaderCallback)callback -{ - RCTAssertParam(request); - RCTAssertParam(handler); - RCTAssertParam(callback); - - if ((self = [super init])) { - _callback = callback; - _request = [[RCTActiveURLRequest alloc] init]; - _request.request = request; - _request.handler = handler; - _request.incrementalUpdates = NO; - _requestToken = [handler sendRequest:request withDelegate:self]; - } - return self; -} - -- (instancetype)init -{ - return [self initWithRequest:nil handler:nil callback:nil]; -} - -- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total -{ - RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); -} - -- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response -{ - RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); - _request.response = response; -} - -- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data -{ - RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); - [_request.data appendData:data]; -} - -- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error -{ - RCTAssert(_callback != nil, @"The callback property must be set"); - _callback(_request.data, _request.response.MIMEType, error); + return _callback(nil, @{@"body": multipartBody, @"contentType": contentType}); } @end @@ -214,8 +121,7 @@ - (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error */ @implementation RCTNetworking { - NSInteger _currentRequestID; - NSMapTable *_activeRequests; + NSMutableDictionary *_tasksByRequestID; } @synthesize bridge = _bridge; @@ -226,16 +132,13 @@ @implementation RCTNetworking - (instancetype)init { if ((self = [super init])) { - _currentRequestID = 0; - _activeRequests = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory - valueOptions:NSPointerFunctionsStrongMemory - capacity:0]; + _tasksByRequestID = [[NSMutableDictionary alloc] init]; } return self; } -- (void)buildRequest:(NSDictionary *)query - completionBlock:(void (^)(NSURLRequest *request))block +- (RCTURLRequestCancellationBlock)buildRequest:(NSDictionary *)query + completionBlock:(void (^)(NSURLRequest *request))block { NSURL *URL = [RCTConvert NSURL:query[@"url"]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; @@ -243,11 +146,11 @@ - (void)buildRequest:(NSDictionary *)query request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]]; NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]]; - [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) { + return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) { if (error) { RCTLogError(@"Error processing request body: %@", error); // Ideally we'd circle back to JS here and notify an error/abort on the request. - return; + return (RCTURLRequestCancellationBlock)nil; } request.HTTPBody = result[@"body"]; NSString *contentType = result[@"contentType"]; @@ -262,6 +165,7 @@ - (void)buildRequest:(NSDictionary *)query } block(request); + return (RCTURLRequestCancellationBlock)nil; }]; } @@ -316,71 +220,56 @@ - (void)buildRequest:(NSDictionary *)query * - @"contentType" (NSString): the content type header of the request * */ -- (void)processDataForHTTPQuery:(NSDictionary *)query callback:(void (^)(NSError *error, NSDictionary *result))callback +- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)query callback: +(RCTURLRequestCancellationBlock (^)(NSError *error, NSDictionary *result))callback { if (!query) { - callback(nil, nil); - return; + return callback(nil, nil); } NSData *body = [RCTConvert NSData:query[@"string"]]; if (body) { - callback(nil, @{@"body": body}); - return; + return callback(nil, @{@"body": body}); } NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]]; if (request) { + id handler = [self handlerForRequest:request]; if (!handler) { - return; + return callback(nil, nil); } - (void)[[RCTDataLoader alloc] initWithRequest:request handler:handler callback:^(NSData *data, NSString *MIMEType, NSError *error) { - if (data) { - callback(nil, @{@"body": data, @"contentType": MIMEType}); - } else { - callback(error, nil); - } + + __block RCTURLRequestCancellationBlock cancellationBlock = nil; + RCTDownloadTask *task = [[RCTDownloadTask alloc] initWithRequest:request handler:handler completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) { + cancellationBlock = callback(error, data ? @{@"body": data, @"contentType": RCTNullIfNil(response.MIMEType)} : nil); }]; - return; + + __weak RCTDownloadTask *weakTask = task; + return ^{ + [weakTask cancel]; + if (cancellationBlock) { + cancellationBlock(); + } + }; } NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:query[@"formData"]]; - if (formData != nil) { + if (formData) { RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init]; - formDataHelper.dataManager = self; - [formDataHelper process:formData callback:callback]; - return; + formDataHelper.networker = self; + return [formDataHelper process:formData callback:callback]; } // Nothing in the data payload, at least nothing we could understand anyway. // Ignore and treat it as if it were null. - callback(nil, nil); -} - -- (void)sendRequest:(NSURLRequest *)request - incrementalUpdates:(BOOL)incrementalUpdates - responseSender:(RCTResponseSenderBlock)responseSender -{ - id handler = [self handlerForRequest:request]; - id token = [handler sendRequest:request withDelegate:self]; - if (token) { - RCTActiveURLRequest *activeRequest = [[RCTActiveURLRequest alloc] init]; - activeRequest.requestID = @(++_currentRequestID); - activeRequest.request = request; - activeRequest.handler = handler; - activeRequest.incrementalUpdates = incrementalUpdates; - [_activeRequests setObject:activeRequest forKey:token]; - responseSender(@[activeRequest.requestID]); - } + return callback(nil, nil); } -- (void)sendData:(NSData *)data forRequestToken:(id)requestToken +- (void)sendData:(NSData *)data forTask:(RCTDownloadTask *)task { if (data.length == 0) { return; } - RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; - // Get text encoding - NSURLResponse *response = request.response; + NSURLResponse *response = task.response; NSStringEncoding encoding = NSUTF8StringEncoding; if (response.textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); @@ -393,82 +282,81 @@ - (void)sendData:(NSData *)data forRequestToken:(id)requestToken return; } - NSArray *responseJSON = @[request.requestID, responseText ?: @""]; + NSArray *responseJSON = @[task.requestID, responseText ?: @""]; [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" body:responseJSON]; } -#pragma mark - RCTURLRequestDelegate - -- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total -{ - dispatch_async(_methodQueue, ^{ - RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; - RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); - - NSArray *responseJSON = @[request.requestID, @(progress), @(total)]; - [_bridge.eventDispatcher sendDeviceEventWithName:@"didUploadProgress" body:responseJSON]; - }); -} - -- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response -{ - dispatch_async(_methodQueue, ^{ - RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; - RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); - - request.response = response; - - NSHTTPURLResponse *httpResponse = nil; - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - // Might be a local file request - httpResponse = (NSHTTPURLResponse *)response; - } - - NSArray *responseJSON = @[request.requestID, - @(httpResponse.statusCode ?: 200), - httpResponse.allHeaderFields ?: @{}, - ]; - - [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" - body:responseJSON]; - }); -} - -- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data +- (void)sendRequest:(NSURLRequest *)request + incrementalUpdates:(BOOL)incrementalUpdates + responseSender:(RCTResponseSenderBlock)responseSender { - dispatch_async(_methodQueue, ^{ - RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; - RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); + id handler = [self handlerForRequest:request]; + if (!handler) { + return; + } - if (request.incrementalUpdates) { - [self sendData:data forRequestToken:requestToken]; - } else { - [request.data appendData:data]; - } - }); -} + __block RCTDownloadTask *task; + + RCTURLRequestProgressBlock uploadProgressBlock = ^(double progress, double total) { + dispatch_async(_methodQueue, ^{ + NSArray *responseJSON = @[task.requestID, @(progress), @(total)]; + [_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON]; + }); + }; + + void (^responseBlock)(NSURLResponse *) = ^(NSURLResponse *response) { + dispatch_async(_methodQueue, ^{ + NSHTTPURLResponse *httpResponse = nil; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + // Might be a local file request + httpResponse = (NSHTTPURLResponse *)response; + } + NSArray *responseJSON = @[task.requestID, + @(httpResponse.statusCode ?: 200), + httpResponse.allHeaderFields ?: @{}, + ]; + + [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" + body:responseJSON]; + }); + }; + + void (^incrementalDataBlock)(NSData *) = incrementalUpdates ? ^(NSData *data) { + dispatch_async(_methodQueue, ^{ + [self sendData:data forTask:task]; + }); + } : nil; + + RCTURLRequestCompletionBlock completionBlock = + ^(NSURLResponse *response, NSData *data, NSError *error) { + dispatch_async(_methodQueue, ^{ + if (!incrementalUpdates) { + [self sendData:data forTask:task]; + } + NSArray *responseJSON = @[task.requestID, + RCTNullIfNil(error.localizedDescription), + ]; -- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error -{ - dispatch_async(_methodQueue, ^{ - RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; - RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); + [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" + body:responseJSON]; - if (!request.incrementalUpdates) { - [self sendData:request.data forRequestToken:requestToken]; - } + [_tasksByRequestID removeObjectForKey:task.requestID]; + }); + }; - NSArray *responseJSON = @[ - request.requestID, - RCTNullIfNil(error.localizedDescription), - ]; + task = [[RCTDownloadTask alloc] initWithRequest:request + handler:handler + completionBlock:completionBlock]; - [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" - body:responseJSON]; + task.incrementalDataBlock = incrementalDataBlock; + task.responseBlock = responseBlock; + task.uploadProgressBlock = uploadProgressBlock; - [_activeRequests removeObjectForKey:requestToken]; - }); + if (task.requestID) { + _tasksByRequestID[task.requestID] = task; + responseSender(@[task.requestID]); + } } #pragma mark - JS API @@ -476,31 +364,22 @@ - (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender) { + // TODO: buildRequest returns a cancellation block, but there's currently + // no way to invoke it, if, for example the request is cancelled while + // loading a large file to build the request body [self buildRequest:query completionBlock:^(NSURLRequest *request) { BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]]; - [self sendRequest:request incrementalUpdates:incrementalUpdates + [self sendRequest:request + incrementalUpdates:incrementalUpdates responseSender:responseSender]; }]; } RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID) { - id requestToken = nil; - RCTActiveURLRequest *activeRequest = nil; - for (id token in _activeRequests) { - RCTActiveURLRequest *request = [_activeRequests objectForKey:token]; - if ([request.requestID isEqualToNumber:requestID]) { - activeRequest = request; - requestToken = token; - break; - } - } - - id handler = activeRequest.handler; - if ([handler respondsToSelector:@selector(cancelRequest:)]) { - [activeRequest.handler cancelRequest:requestToken]; - } + [_tasksByRequestID[requestID] cancel]; + [_tasksByRequestID removeObjectForKey:requestID]; } @end diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index da020c808f5863..7b84e35d1f62b1 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -35,7 +35,7 @@ class XMLHttpRequest extends XMLHttpRequestBase { _didCreateRequest(requestId: number): void { this._requestId = requestId; this._subscriptions.push(RCTDeviceEventEmitter.addListener( - 'didUploadProgress', + 'didSendNetworkData', (args) => this._didUploadProgress.call(this, args[0], args[1], args[2]) )); this._subscriptions.push(RCTDeviceEventEmitter.addListener( diff --git a/React/Base/RCTURLRequestDelegate.h b/React/Base/RCTURLRequestDelegate.h index 48473b84ba399e..f038d1d6a998a5 100644 --- a/React/Base/RCTURLRequestDelegate.h +++ b/React/Base/RCTURLRequestDelegate.h @@ -16,10 +16,10 @@ @protocol RCTURLRequestDelegate /** - * Call this when you first receives a response from the server. This should - * include response headers, etc. + * Call this when you send request data to the server. This is used to track + * upload progress, so should be called multiple times for large request bodies. */ -- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total; +- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent; /** * Call this when you first receives a response from the server. This should From 19a8eff668e8b1c26a4be0e85144390358b574e1 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Thu, 23 Jul 2015 06:36:37 -0700 Subject: [PATCH 7/9] [ReactNative] ScrollView docs Summary: In preparation for open sourcing React Native for Android, document which ScrollView props are platform-specific. --- Libraries/Components/ScrollView/ScrollView.js | 91 ++++++++++++++++--- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 512d8dbd6a4192..48dd3c4659c699 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -53,44 +53,54 @@ var INNERVIEW = 'InnerScrollView'; * Doesn't yet support other contained responders from blocking this scroll * view from becoming the responder. */ - var ScrollView = React.createClass({ propTypes: { - automaticallyAdjustContentInsets: PropTypes.bool, // true - contentInset: EdgeInsetsPropType, // zeros - contentOffset: PointPropType, // zeros - onScroll: PropTypes.func, - onScrollAnimationEnd: PropTypes.func, - scrollEnabled: PropTypes.bool, // true - scrollIndicatorInsets: EdgeInsetsPropType, // zeros - showsHorizontalScrollIndicator: PropTypes.bool, - showsVerticalScrollIndicator: PropTypes.bool, - style: StyleSheetPropType(ViewStylePropTypes), - scrollEventThrottle: PropTypes.number, // null - + /** + * Controls whether iOS should automatically adjust the content inset + * for scroll views that are placed behind a navigation bar or + * tab bar/ toolbar. The default value is true. + * @platform ios + */ + automaticallyAdjustContentInsets: PropTypes.bool, + /** + * The amount by which the scroll view content is inset from the edges + * of the scroll view. Defaults to `{0, 0, 0, 0}`. + * @platform ios + */ + contentInset: EdgeInsetsPropType, + /** + * Used to manually set the starting scroll offset. + * The default value is `{x: 0, y: 0}`. + * @platform ios + */ + contentOffset: PointPropType, /** * When true, the scroll view bounces when it reaches the end of the * content if the content is larger then the scroll view along the axis of * the scroll direction. When false, it disables all bouncing even if * the `alwaysBounce*` props are true. The default value is true. + * @platform ios */ bounces: PropTypes.bool, /** * When true, gestures can drive zoom past min/max and the zoom will animate * to the min/max value at gesture end, otherwise the zoom will not exceed * the limits. + * @platform ios */ bouncesZoom: PropTypes.bool, /** * When true, the scroll view bounces horizontally when it reaches the end * even if the content is smaller than the scroll view itself. The default * value is true when `horizontal={true}` and false otherwise. + * @platform ios */ alwaysBounceHorizontal: PropTypes.bool, /** * When true, the scroll view bounces vertically when it reaches the end * even if the content is smaller than the scroll view itself. The default * value is false when `horizontal={true}` and true otherwise. + * @platform ios */ alwaysBounceVertical: PropTypes.bool, /** @@ -98,6 +108,7 @@ var ScrollView = React.createClass({ * content is smaller than the scroll view bounds; when the content is * larger than the scroll view, this property has no effect. The default * value is false. + * @platform ios */ centerContent: PropTypes.bool, /** @@ -121,6 +132,7 @@ var ScrollView = React.createClass({ * decelerates after the user lifts their finger. Reasonable choices include * - Normal: 0.998 (the default) * - Fast: 0.9 + * @platform ios */ decelerationRate: PropTypes.number, /** @@ -131,17 +143,19 @@ var ScrollView = React.createClass({ /** * When true, the ScrollView will try to lock to only vertical or horizontal * scrolling while dragging. The default value is false. + * @platform ios */ directionalLockEnabled: PropTypes.bool, /** * When false, once tracking starts, won't try to drag if the touch moves. * The default value is true. + * @platform ios */ canCancelContentTouches: PropTypes.bool, /** * Determines whether the keyboard gets dismissed in response to a drag. * - 'none' (the default), drags do not dismiss the keyboard. - * - 'onDrag', the keyboard is dismissed when a drag begins. + * - 'on-drag', the keyboard is dismissed when a drag begins. * - 'interactive', the keyboard is dismissed interactively with the drag * and moves in synchrony with the touch; dragging upwards cancels the * dismissal. @@ -156,35 +170,83 @@ var ScrollView = React.createClass({ * is up dismisses the keyboard. When true, the scroll view will not catch * taps, and the keyboard will not dismiss automatically. The default value * is false. + * @platform ios */ keyboardShouldPersistTaps: PropTypes.bool, /** * The maximum allowed zoom scale. The default value is 1.0. + * @platform ios */ maximumZoomScale: PropTypes.number, /** * The minimum allowed zoom scale. The default value is 1.0. + * @platform ios */ minimumZoomScale: PropTypes.number, + /** + * Fires at most once per frame during scrolling. The frequency of the + * events can be contolled using the `scrollEventThrottle` prop. + */ + onScroll: PropTypes.func, + /** + * Called when a scrolling animation ends. + * @platform ios + */ + onScrollAnimationEnd: PropTypes.func, /** * When true, the scroll view stops on multiples of the scroll view's size * when scrolling. This can be used for horizontal pagination. The default * value is false. + * @platform ios */ pagingEnabled: PropTypes.bool, + /** + * When false, the content does not scroll. + * The default value is true. + * @platform ios + */ + scrollEnabled: PropTypes.bool, + /** + * This controls how often the scroll event will be fired while scrolling + * (in events per seconds). A higher number yields better accuracy for code + * that is tracking the scroll position, but can lead to scroll performance + * problems due to the volume of information being send over the bridge. + * The default value is zero, which means the scroll event will be sent + * only once each time the view is scrolled. + * @platform ios + */ + scrollEventThrottle: PropTypes.number, + /** + * The amount by which the scroll view indicators are inset from the edges + * of the scroll view. This should normally be set to the same value as + * the `contentInset`. Defaults to `{0, 0, 0, 0}`. + * @platform ios + */ + scrollIndicatorInsets: EdgeInsetsPropType, /** * When true, the scroll view scrolls to top when the status bar is tapped. * The default value is true. + * @platform ios */ scrollsToTop: PropTypes.bool, + /** + * When true, shows a horizontal scroll indicator. + */ + showsHorizontalScrollIndicator: PropTypes.bool, + /** + * When true, shows a vertical scroll indicator. + */ + showsVerticalScrollIndicator: PropTypes.bool, /** * An array of child indices determining which children get docked to the * top of the screen when scrolling. For example, passing * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the * top of the scroll view. This property is not supported in conjunction * with `horizontal={true}`. + * @platform ios */ stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), + style: StyleSheetPropType(ViewStylePropTypes), /** * Experimental: When true, offscreen child views (whose `overflow` value is * `hidden`) are removed from their native backing superview when offscreen. @@ -194,6 +256,7 @@ var ScrollView = React.createClass({ removeClippedSubviews: PropTypes.bool, /** * The current scale of the scroll view content. The default value is 1.0. + * @platform ios */ zoomScale: PropTypes.number, }, From 9c8ba9502ca203192e75b9e1c8412763895c907c Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Thu, 23 Jul 2015 06:43:35 -0700 Subject: [PATCH 8/9] [react_native] Refactor UIExplorerList to fix UIExplorerApp --- Examples/UIExplorer/UIExplorerApp.ios.js | 2 +- Examples/UIExplorer/UIExplorerList.ios.js | 176 +++++++++++++++++++ Examples/UIExplorer/UIExplorerListBase.js | 195 ++++++++++++++++++++++ 3 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 Examples/UIExplorer/UIExplorerList.ios.js create mode 100644 Examples/UIExplorer/UIExplorerListBase.js diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js index e5bfd22a374b41..c2f4734e85e428 100644 --- a/Examples/UIExplorer/UIExplorerApp.ios.js +++ b/Examples/UIExplorer/UIExplorerApp.ios.js @@ -17,7 +17,7 @@ 'use strict'; var React = require('react-native'); -var UIExplorerList = require('./UIExplorerList'); +var UIExplorerList = require('./UIExplorerList.ios'); var { AppRegistry, NavigatorIOS, diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js new file mode 100644 index 00000000000000..2e1c444f10eaee --- /dev/null +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -0,0 +1,176 @@ +/** + * 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 { + AppRegistry, + Settings, + StyleSheet, +} = React; + +var { TestModule } = React.addons; + +import type { NavigationContext } from 'NavigationContext'; + +var UIExplorerListBase = require('./UIExplorerListBase'); + +var COMPONENTS = [ + require('./ActivityIndicatorIOSExample'), + require('./DatePickerIOSExample'), + require('./ImageExample'), + require('./LayoutEventsExample'), + require('./ListViewExample'), + require('./ListViewGridLayoutExample'), + require('./ListViewPagingExample'), + require('./MapViewExample'), + require('./Navigator/NavigatorExample'), + require('./NavigatorIOSColorsExample'), + require('./NavigatorIOSExample'), + require('./PickerIOSExample'), + require('./ProgressViewIOSExample'), + require('./ScrollViewExample'), + require('./SegmentedControlIOSExample'), + require('./SliderIOSExample'), + require('./SwitchIOSExample'), + require('./TabBarIOSExample'), + require('./TextExample.ios'), + require('./TextInputExample'), + require('./TouchableExample'), + require('./ViewExample'), + require('./WebViewExample'), +]; + +var APIS = [ + require('./AccessibilityIOSExample'), + require('./ActionSheetIOSExample'), + require('./AdSupportIOSExample'), + require('./AlertIOSExample'), + require('./AnimationExample/AnExApp'), + require('./AppStateIOSExample'), + require('./AsyncStorageExample'), + require('./BorderExample'), + require('./CameraRollExample.ios'), + require('./GeolocationExample'), + require('./LayoutExample'), + require('./NetInfoExample'), + require('./PanResponderExample'), + require('./PointerEventsExample'), + require('./PushNotificationIOSExample'), + require('./StatusBarIOSExample'), + require('./TimerExample'), + require('./VibrationIOSExample'), + require('./XHRExample'), +]; + +// Register suitable examples for snapshot tests +COMPONENTS.concat(APIS).forEach((Example) => { + if (Example.displayName) { + var Snapshotter = React.createClass({ + componentDidMount: function() { + // View is still blank after first RAF :\ + global.requestAnimationFrame(() => + global.requestAnimationFrame(() => TestModule.verifySnapshot( + TestModule.markTestPassed + ) + )); + }, + render: function() { + var Renderable = UIExplorerListBase.makeRenderable(Example); + return ; + }, + }); + AppRegistry.registerComponent(Example.displayName, () => Snapshotter); + } +}); + +type Props = { + navigator: { + navigationContext: NavigationContext, + push: (route: {title: string, component: ReactClass}) => void, + }, + onExternalExampleRequested: Function, +}; + +class UIExplorerList extends React.Component { + props: Props; + + render() { + return ( + + ); + } + + componentWillMount() { + this.props.navigator.navigationContext.addListener('didfocus', function(event) { + if (event.data.route.title === 'UIExplorer') { + Settings.set({visibleExample: null}); + } + }); + } + + componentDidMount() { + var visibleExampleTitle = Settings.get('visibleExample'); + if (visibleExampleTitle) { + var predicate = (example) => example.title === visibleExampleTitle; + var foundExample = APIS.find(predicate) || COMPONENTS.find(predicate); + if (foundExample) { + setTimeout(() => this._openExample(foundExample), 100); + } + } + } + + renderAdditionalView(renderRow: Function, renderTextInput: Function): React.Component { + return renderTextInput(styles.searchTextInput); + } + + search(text: mixed) { + Settings.set({searchText: text}); + } + + _openExample(example: any) { + if (example.external) { + this.props.onExternalExampleRequested(example); + return; + } + + var Component = UIExplorerListBase.makeRenderable(example); + this.props.navigator.push({ + title: Component.title, + component: Component, + }); + } + + onPressRow(example: any) { + Settings.set({visibleExample: example.title}); + this._openExample(example); + } +} + +var styles = StyleSheet.create({ + searchTextInput: { + height: 30, + }, +}); + +module.exports = UIExplorerList; diff --git a/Examples/UIExplorer/UIExplorerListBase.js b/Examples/UIExplorer/UIExplorerListBase.js new file mode 100644 index 00000000000000..4b26a6976dc6c6 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerListBase.js @@ -0,0 +1,195 @@ +/** + * 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 { + ListView, + PixelRatio, + StyleSheet, + Text, + TextInput, + TouchableHighlight, + View, +} = React; +var createExamplePage = require('./createExamplePage'); + +var ds = new ListView.DataSource({ + rowHasChanged: (r1, r2) => r1 !== r2, + sectionHeaderHasChanged: (h1, h2) => h1 !== h2, +}); + +class UIExplorerListBase extends React.Component { + constructor(props: any) { + super(props); + this.state = { + dataSource: ds.cloneWithRowsAndSections({ + components: [], + apis: [], + }), + searchText: this.props.searchText, + }; + } + + componentDidMount(): void { + this.search(this.state.searchText); + } + + render() { + var topView = this.props.renderAdditionalView && + this.props.renderAdditionalView(this.renderRow.bind(this), this.renderTextInput.bind(this)); + + return ( + + {topView} + + + ); + } + + renderTextInput(searchTextInputStyle: any) { + return ( + + + + ); + } + + _renderSectionHeader(data: any, section: string) { + return ( + + + {section.toUpperCase()} + + + ); + } + + renderRow(example: any, i: number) { + return ( + + this.onPressRow(example)}> + + + {example.title} + + + {example.description} + + + + + + ); + } + + search(text: mixed): void { + this.props.search && this.props.search(text); + + var regex = new RegExp(text, 'i'); + var filter = (component) => regex.test(component.title); + + this.setState({ + dataSource: ds.cloneWithRowsAndSections({ + components: this.props.components.filter(filter), + apis: this.props.apis.filter(filter), + }), + searchText: text, + }); + } + + onPressRow(example: any): void { + this.props.onPressRow && this.props.onPressRow(example); + } + + static makeRenderable(example: any): ReactClass { + return example.examples ? + createExamplePage(null, example) : + example; + } +} + +var styles = StyleSheet.create({ + listContainer: { + flex: 1, + }, + list: { + backgroundColor: '#eeeeee', + }, + sectionHeader: { + padding: 5, + }, + group: { + backgroundColor: 'white', + }, + sectionHeaderTitle: { + fontWeight: '500', + fontSize: 11, + }, + row: { + backgroundColor: 'white', + justifyContent: 'center', + paddingHorizontal: 15, + paddingVertical: 8, + }, + separator: { + height: 1 / PixelRatio.get(), + backgroundColor: '#bbbbbb', + marginLeft: 15, + }, + rowTitleText: { + fontSize: 17, + fontWeight: '500', + }, + rowDetailText: { + fontSize: 15, + color: '#888888', + lineHeight: 20, + }, + searchRow: { + backgroundColor: '#eeeeee', + paddingTop: 75, + paddingLeft: 10, + paddingRight: 10, + paddingBottom: 10, + }, + searchTextInput: { + backgroundColor: 'white', + borderColor: '#cccccc', + borderRadius: 3, + borderWidth: 1, + paddingLeft: 8, + }, +}); + +module.exports = UIExplorerListBase; From 064dafa618d077392100d712d40a553aecf2b3e3 Mon Sep 17 00:00:00 2001 From: Daniel Brockman Date: Thu, 23 Jul 2015 12:05:40 -0700 Subject: [PATCH 9/9] #!/bin/bash => #!/usr/bin/env bash Summary: This change makes `npm start` work correctly on e.g. NixOS. Closes https://github.com/facebook/react-native/pull/2006 Github Author: Daniel Brockman --- packager/launchPackager.command | 2 +- packager/packager.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packager/launchPackager.command b/packager/launchPackager.command index eb777493fa4097..0537f7c84d8105 100755 --- a/packager/launchPackager.command +++ b/packager/launchPackager.command @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright (c) 2015-present, Facebook, Inc. # All rights reserved. diff --git a/packager/packager.sh b/packager/packager.sh index f763b9bae2faf4..95cd8ce1e60c10 100755 --- a/packager/packager.sh +++ b/packager/packager.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright (c) 2015-present, Facebook, Inc. # All rights reserved.