diff --git a/Examples/UIExplorer/CameraRollExample.ios.js b/Examples/UIExplorer/CameraRollExample.ios.js
new file mode 100644
index 000000000..8037f536f
--- /dev/null
+++ b/Examples/UIExplorer/CameraRollExample.ios.js
@@ -0,0 +1,115 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule CameraRollExample
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ CameraRoll,
+ Image,
+ Slider,
+ StyleSheet,
+ SwitchIOS,
+ Text,
+ View,
+} = React;
+
+var CameraRollView = require('./CameraRollView.ios');
+
+var CAMERA_ROLL_VIEW = 'camera_roll_view';
+
+var CameraRollExample = React.createClass({
+
+ getInitialState() {
+ return {
+ groupTypes: 'SavedPhotos',
+ sliderValue: 1,
+ bigImages: true,
+ };
+ },
+
+ render() {
+ return (
+
+
+ {(this.state.bigImages ? 'Big' : 'Small') + ' Images'}
+
+ {'Group Type: ' + this.state.groupTypes}
+
+
+ );
+ },
+
+ _renderImage(asset) {
+ var imageSize = this.state.bigImages ? 150 : 75;
+ var imageStyle = [styles.image, {width: imageSize, height: imageSize}];
+ var location = asset.node.location.longitude ?
+ JSON.stringify(asset.node.location) : 'Unknown location';
+ return (
+
+
+
+ {asset.node.image.uri}
+ {location}
+ {asset.node.group_name}
+ {new Date(asset.node.timestamp).toString()}
+
+
+ );
+ },
+
+ _onSliderChange(value) {
+ var options = CameraRoll.GroupTypesOptions;
+ var index = Math.floor(value * options.length * 0.99);
+ var groupTypes = options[index];
+ if (groupTypes !== this.state.groupTypes) {
+ this.setState({groupTypes: groupTypes});
+ }
+ },
+
+ _onSwitchChange(value) {
+ this.refs[CAMERA_ROLL_VIEW].rendererChanged();
+ this.setState({ bigImages: value });
+ }
+});
+
+var styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ flex: 1,
+ },
+ url: {
+ fontSize: 9,
+ marginBottom: 14,
+ },
+ image: {
+ margin: 4,
+ },
+ info: {
+ flex: 1,
+ },
+});
+
+exports.title = '';
+exports.description = 'Example component that uses CameraRoll to list user\'s photos';
+exports.examples = [
+ {
+ title: 'Photos',
+ render() { return ; }
+ }
+];
diff --git a/Examples/UIExplorer/CameraRollView.ios.js b/Examples/UIExplorer/CameraRollView.ios.js
new file mode 100644
index 000000000..f0ee92afc
--- /dev/null
+++ b/Examples/UIExplorer/CameraRollView.ios.js
@@ -0,0 +1,231 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule CameraRollView
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ ActivityIndicatorIOS,
+ CameraRoll,
+ Image,
+ ListView,
+ ListViewDataSource,
+ StyleSheet,
+ View,
+} = React;
+
+var groupByEveryN = require('groupByEveryN');
+var logError = require('logError');
+
+var propTypes = {
+ /**
+ * The group where the photos will be fetched from. Possible
+ * values are 'Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream'
+ * and SavedPhotos.
+ */
+ groupTypes: React.PropTypes.oneOf([
+ 'Album',
+ 'All',
+ 'Event',
+ 'Faces',
+ 'Library',
+ 'PhotoStream',
+ 'SavedPhotos',
+ ]),
+
+ /**
+ * Number of images that will be fetched in one page.
+ */
+ batchSize: React.PropTypes.number,
+
+ /**
+ * A function that takes a single image as a parameter and renders it.
+ */
+ renderImage: React.PropTypes.func,
+
+ /**
+ * imagesPerRow: Number of images to be shown in each row.
+ */
+ imagesPerRow: React.PropTypes.number,
+};
+
+var CameraRollView = React.createClass({
+ propTypes: propTypes,
+
+ getDefaultProps: function() {
+ return {
+ groupTypes: 'SavedPhotos',
+ batchSize: 5,
+ imagesPerRow: 1,
+ renderImage: function(asset) {
+ var imageSize = 150;
+ var imageStyle = [styles.image, {width: imageSize, height: imageSize}];
+ return (
+
+ );
+ },
+ };
+ },
+
+ getInitialState: function() {
+ var ds = new ListViewDataSource({rowHasChanged: this._rowHasChanged});
+
+ return {
+ assets: [],
+ groupTypes: this.props.groupTypes,
+ lastCursor: null,
+ noMore: false,
+ loadingMore: false,
+ dataSource: ds,
+ };
+ },
+
+ /**
+ * This should be called when the image renderer is changed to tell the
+ * component to re-render its assets.
+ */
+ rendererChanged: function() {
+ var ds = new ListViewDataSource({rowHasChanged: this._rowHasChanged});
+ this.state.dataSource = ds.cloneWithRows(
+ groupByEveryN(this.state.assets, this.props.imagesPerRow)
+ );
+ },
+
+ componentDidMount: function() {
+ this.fetch();
+ },
+
+ componentWillReceiveProps: function(nextProps) {
+ if (this.props.groupTypes !== nextProps.groupTypes) {
+ this.fetch(true);
+ }
+ },
+
+ _fetch: function(clear) {
+ if (clear) {
+ this.setState(this.getInitialState(), this.fetch);
+ return;
+ }
+
+ var fetchParams = {
+ first: this.props.batchSize,
+ groupTypes: this.props.groupTypes,
+ };
+ if (this.state.lastCursor) {
+ fetchParams.after = this.state.lastCursor;
+ }
+
+ CameraRoll.getPhotos(fetchParams, this._appendAssets, logError);
+ },
+
+ /**
+ * Fetches more images from the camera roll. If clear is set to true, it will
+ * set the component to its initial state and re-fetch the images.
+ */
+ fetch: function(clear) {
+ if (!this.state.loadingMore) {
+ this.setState({loadingMore: true}, () => { this._fetch(clear); });
+ }
+ },
+
+ render: function() {
+ return (
+
+ );
+ },
+
+ _rowHasChanged: function(r1, r2) {
+ if (r1.length !== r2.length) {
+ return true;
+ }
+
+ for (var i = 0; i < r1.length; i++) {
+ if (r1[i] !== r2[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ _renderFooterSpinner: function() {
+ if (!this.state.noMore) {
+ return ;
+ }
+ return null;
+ },
+
+ // rowData is an array of images
+ _renderRow: function(rowData, sectionID, rowID) {
+ var images = rowData.map((image) => {
+ if (image === null) {
+ return null;
+ }
+ return this.props.renderImage(image);
+ });
+
+ return (
+
+ {images}
+
+ );
+ },
+
+ _appendAssets: function(data) {
+ var assets = data.edges;
+ var newState = { loadingMore: false };
+
+ if (!data.page_info.has_next_page) {
+ newState.noMore = true;
+ }
+
+ if (assets.length > 0) {
+ newState.lastCursor = data.page_info.end_cursor;
+ newState.assets = this.state.assets.concat(assets);
+ newState.dataSource = this.state.dataSource.cloneWithRows(
+ groupByEveryN(newState.assets, this.props.imagesPerRow)
+ );
+ }
+
+ this.setState(newState);
+ },
+
+ _onEndReached: function() {
+ if (!this.state.noMore) {
+ this.fetch();
+ }
+ },
+});
+
+var styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ flex: 1,
+ },
+ url: {
+ fontSize: 9,
+ marginBottom: 14,
+ },
+ image: {
+ margin: 4,
+ },
+ info: {
+ flex: 1,
+ },
+ container: {
+ flex: 1,
+ },
+});
+
+module.exports = CameraRollView;
diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js
index e04edcab6..85627ee79 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -36,6 +36,7 @@ var EXAMPLES = [
require('./TabBarExample'),
require('./SwitchExample'),
require('./SliderExample'),
+ require('./CameraRollExample.ios'),
];
var UIExplorerList = React.createClass({
diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js
new file mode 100644
index 000000000..54295fc52
--- /dev/null
+++ b/Libraries/CameraRoll/CameraRoll.js
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule CameraRoll
+ */
+'use strict';
+
+var ReactPropTypes = require('ReactPropTypes');
+var RKCameraRollManager = require('NativeModules').RKCameraRollManager;
+
+var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker');
+var deepFreezeAndThrowOnMutationInDev =
+ require('deepFreezeAndThrowOnMutationInDev');
+var invariant = require('invariant');
+
+var GROUP_TYPES_OPTIONS = [
+ 'Album',
+ 'All',
+ 'Event',
+ 'Faces',
+ 'Library',
+ 'PhotoStream',
+ 'SavedPhotos', // default
+];
+
+deepFreezeAndThrowOnMutationInDev(GROUP_TYPES_OPTIONS);
+
+/**
+ * Shape of the param arg for the `getPhotos` function.
+ */
+var getPhotosParamChecker = createStrictShapeTypeChecker({
+ /**
+ * The number of photos wanted in reverse order of the photo application
+ * (i.e. most recent first for SavedPhotos).
+ */
+ first: ReactPropTypes.number.isRequired,
+
+ /**
+ * A cursor that matches `page_info { end_cursor }` returned from a previous
+ * call to `getPhotos`
+ */
+ after: ReactPropTypes.string,
+
+ /**
+ * Specifies which group types to filter the results to.
+ */
+ groupTypes: ReactPropTypes.oneOf(GROUP_TYPES_OPTIONS),
+
+ /**
+ * Specifies filter on group names, like 'Recent Photos' or custom album
+ * titles.
+ */
+ groupName: ReactPropTypes.string,
+});
+
+/**
+ * Shape of the return value of the `getPhotos` function.
+ */
+var getPhotosReturnChecker = createStrictShapeTypeChecker({
+ edges: ReactPropTypes.arrayOf(createStrictShapeTypeChecker({
+ node: createStrictShapeTypeChecker({
+ type: ReactPropTypes.string.isRequired,
+ group_name: ReactPropTypes.string.isRequired,
+ image: createStrictShapeTypeChecker({
+ uri: ReactPropTypes.string.isRequired,
+ height: ReactPropTypes.number.isRequired,
+ width: ReactPropTypes.number.isRequired,
+ isStored: ReactPropTypes.bool,
+ }).isRequired,
+ timestamp: ReactPropTypes.number.isRequired,
+ location: createStrictShapeTypeChecker({
+ latitude: ReactPropTypes.number,
+ longitude: ReactPropTypes.number,
+ altitude: ReactPropTypes.number,
+ heading: ReactPropTypes.number,
+ speed: ReactPropTypes.number,
+ }),
+ }).isRequired,
+ })).isRequired,
+ page_info: createStrictShapeTypeChecker({
+ has_next_page: ReactPropTypes.bool.isRequired,
+ start_cursor: ReactPropTypes.string,
+ end_cursor: ReactPropTypes.string,
+ }).isRequired,
+});
+
+class CameraRoll {
+ /**
+ * Saves the image with tag `tag` to the camera roll.
+ *
+ * @param {string} tag - Can be any of the three kinds of tags we accept:
+ * 1. URL
+ * 2. assets-library tag
+ * 3. tag returned from storing an image in memory
+ */
+ static saveImageWithTag(tag, successCallback, errorCallback) {
+ invariant(
+ typeof tag === 'string',
+ 'CameraRoll.saveImageWithTag tag must be a valid string.'
+ );
+ RKCameraRollManager.saveImageWithTag(
+ tag,
+ (imageTag) => {
+ successCallback && successCallback(imageTag);
+ },
+ (errorMessage) => {
+ errorCallback && errorCallback(errorMessage);
+ });
+ }
+
+ /**
+ * Invokes `callback` with photo identifier objects from the local camera
+ * roll of the device matching shape defined by `getPhotosReturnChecker`.
+ *
+ * @param {object} params - See `getPhotosParamChecker`.
+ * @param {function} callback - Invoked with arg of shape defined by
+ * `getPhotosReturnChecker` on success.
+ * @param {function} errorCallback - Invoked with error message on error.
+ */
+ static getPhotos(params, callback, errorCallback) {
+ var metaCallback = callback;
+ if (__DEV__) {
+ getPhotosParamChecker({params}, 'params', 'CameraRoll.getPhotos');
+ invariant(
+ typeof callback === 'function',
+ 'CameraRoll.getPhotos callback must be a valid function.'
+ );
+ invariant(
+ typeof errorCallback === 'function',
+ 'CameraRoll.getPhotos errorCallback must be a valid function.'
+ );
+ }
+ if (__DEV__) {
+ metaCallback = (response) => {
+ getPhotosReturnChecker(
+ {response},
+ 'response',
+ 'CameraRoll.getPhotos callback'
+ );
+ callback(response);
+ };
+ }
+ RKCameraRollManager.getPhotos(params, metaCallback, errorCallback);
+ }
+}
+
+CameraRoll.GroupTypesOptions = GROUP_TYPES_OPTIONS;
+
+module.exports = CameraRoll;
diff --git a/Libraries/Image/RCTCameraRollManager.h b/Libraries/Image/RCTCameraRollManager.h
new file mode 100644
index 000000000..4a957d6a2
--- /dev/null
+++ b/Libraries/Image/RCTCameraRollManager.h
@@ -0,0 +1,7 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTBridgeModule.h"
+
+@interface RCTCameraRollManager : NSObject
+
+@end
diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m
new file mode 100644
index 000000000..9f86ffb69
--- /dev/null
+++ b/Libraries/Image/RCTCameraRollManager.m
@@ -0,0 +1,148 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTCameraRollManager.h"
+
+#import
+#import
+#import
+#import
+
+ #import "RCTImageLoader.h"
+#import "RCTLog.h"
+
+@implementation RCTCameraRollManager
+
+- (void)saveImageWithTag:(NSString *)imageTag successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseSenderBlock)errorCallback
+{
+ RCT_EXPORT();
+
+ [RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
+ if (loadError) {
+ errorCallback(@[[loadError localizedDescription]]);
+ return;
+ }
+ [[RCTImageLoader assetsLibrary] writeImageToSavedPhotosAlbum:[loadedImage CGImage] metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
+ if (saveError) {
+ NSString *errorMessage = [NSString stringWithFormat:@"Error saving cropped image: %@", saveError];
+ RCTLogWarn(@"%@", errorMessage);
+ errorCallback(@[errorMessage]);
+ return;
+ }
+ successCallback(@[[assetURL absoluteString]]);
+ }];
+ }];
+}
+
+- (void)callCallback:(RCTResponseSenderBlock)callback withAssets:(NSArray *)assets hasNextPage:(BOOL)hasNextPage
+{
+ if (![assets count]) {
+ callback(@[@{
+ @"edges": assets,
+ @"page_info": @{
+ @"has_next_page": @NO}
+ }]);
+ return;
+ }
+ callback(@[@{
+ @"edges": assets,
+ @"page_info": @{
+ @"start_cursor": assets[0][@"node"][@"image"][@"uri"],
+ @"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"],
+ @"has_next_page": @(hasNextPage)}
+ }]);
+}
+
+- (void)getPhotos:(NSDictionary *)params callback:(RCTResponseSenderBlock)callback errorCallback:(RCTResponseSenderBlock)errorCallback
+{
+ RCT_EXPORT();
+
+ NSUInteger first = [params[@"first"] integerValue];
+ NSString *afterCursor = params[@"after"];
+ NSString *groupTypesStr = params[@"groupTypes"];
+ NSString *groupName = params[@"groupName"];
+ ALAssetsGroupType groupTypes;
+ if ([groupTypesStr isEqualToString:@"Album"]) {
+ groupTypes = ALAssetsGroupAlbum;
+ } else if ([groupTypesStr isEqualToString:@"All"]) {
+ groupTypes = ALAssetsGroupAll;
+ } else if ([groupTypesStr isEqualToString:@"Event"]) {
+ groupTypes = ALAssetsGroupEvent;
+ } else if ([groupTypesStr isEqualToString:@"Faces"]) {
+ groupTypes = ALAssetsGroupFaces;
+ } else if ([groupTypesStr isEqualToString:@"Library"]) {
+ groupTypes = ALAssetsGroupLibrary;
+ } else if ([groupTypesStr isEqualToString:@"PhotoStream"]) {
+ groupTypes = ALAssetsGroupPhotoStream;
+ } else {
+ groupTypes = ALAssetsGroupSavedPhotos;
+ }
+
+ BOOL __block foundAfter = NO;
+ BOOL __block hasNextPage = NO;
+ BOOL __block calledCallback = NO;
+ NSMutableArray *assets = [[NSMutableArray alloc] init];
+
+ [[RCTImageLoader assetsLibrary] enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) {
+ if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) {
+ [group setAssetsFilter:ALAssetsFilter.allPhotos];
+ [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) {
+ if (result) {
+ NSString *uri = [(NSURL *)[result valueForProperty:ALAssetPropertyAssetURL] absoluteString];
+ if (afterCursor && !foundAfter) {
+ if ([afterCursor isEqualToString:uri]) {
+ foundAfter = YES;
+ }
+ return; // Skip until we get to the first one
+ }
+ if (first == [assets count]) {
+ *stopAssets = YES;
+ *stopGroups = YES;
+ hasNextPage = YES;
+ RCTAssert(calledCallback == NO, @"Called the callback before we finished processing the results.");
+ [self callCallback:callback withAssets:assets hasNextPage:hasNextPage];
+ calledCallback = YES;
+ return;
+ }
+ CGSize dimensions = [result defaultRepresentation].dimensions;
+ CLLocation *loc = [result valueForProperty:ALAssetPropertyLocation];
+ NSDate *date = [result valueForProperty:ALAssetPropertyDate];
+ [assets addObject:@{
+ @"node": @{
+ @"type": [result valueForProperty:ALAssetPropertyType],
+ @"group_name": [group valueForProperty:ALAssetsGroupPropertyName],
+ @"image": @{
+ @"uri": uri,
+ @"height": @(dimensions.height),
+ @"width": @(dimensions.width),
+ @"isStored": @YES,
+ },
+ @"timestamp": @([date timeIntervalSince1970]),
+ @"location": loc ?
+ @{
+ @"latitude": @(loc.coordinate.latitude),
+ @"longitude": @(loc.coordinate.longitude),
+ @"altitude": @(loc.altitude),
+ @"heading": @(loc.course),
+ @"speed": @(loc.speed),
+ } : @{},
+ }
+ }];
+ }
+ }];
+ } else {
+ // Sometimes the enumeration continues even if we set stop above, so we guard against calling the callback
+ // multiple times here.
+ if (!calledCallback) {
+ [self callCallback:callback withAssets:assets hasNextPage:hasNextPage];
+ calledCallback = YES;
+ }
+ }
+ } failureBlock:^(NSError *error) {
+ if (error.code != ALAssetsLibraryAccessUserDeniedError) {
+ RCTLogError(@"Failure while iterating through asset groups %@", error);
+ }
+ errorCallback(@[error.description]);
+ }];
+}
+
+@end
diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj
index 409d61d32..dea9cb419 100644
--- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj
+++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj
@@ -10,6 +10,8 @@
1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */; };
1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; };
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; };
+ 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; };
+ 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; };
58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; };
58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */; };
58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */; };
@@ -34,6 +36,10 @@
1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = ""; };
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImage.h; sourceTree = ""; };
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; };
+ 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; };
+ 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; };
+ 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; };
+ 143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = ""; };
58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; };
58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = ""; };
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = ""; };
@@ -57,6 +63,10 @@
58B511541A9E6B3D00147676 = {
isa = PBXGroup;
children = (
+ 143879361AAD32A300F088A5 /* RCTImageLoader.h */,
+ 143879371AAD32A300F088A5 /* RCTImageLoader.m */,
+ 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */,
+ 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */,
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */,
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */,
58B511891A9E6BD600147676 /* RCTImageDownloader.h */,
@@ -142,6 +152,8 @@
1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */,
58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */,
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */,
+ 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */,
+ 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */,
1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h
new file mode 100644
index 000000000..3554f4b46
--- /dev/null
+++ b/Libraries/Image/RCTImageLoader.h
@@ -0,0 +1,13 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import
+
+@class ALAssetsLibrary;
+@class UIImage;
+
+@interface RCTImageLoader : NSObject
+
++ (ALAssetsLibrary *)assetsLibrary;
++ (void)loadImageWithTag:(NSString *)tag callback:(void (^)(NSError *error, UIImage *image))callback;
+
+@end
diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m
new file mode 100644
index 000000000..ec3e1dda2
--- /dev/null
+++ b/Libraries/Image/RCTImageLoader.m
@@ -0,0 +1,98 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTImageLoader.h"
+
+#import
+#import
+#import
+#import
+#import
+
+#import "RCTConvert.h"
+#import "RCTImageDownloader.h"
+#import "RCTLog.h"
+
+NSError *errorWithMessage(NSString *message) {
+ NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
+ NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
+ return error;
+}
+
+@implementation RCTImageLoader
+
++ (ALAssetsLibrary *)assetsLibrary
+{
+ static ALAssetsLibrary *assetsLibrary = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ assetsLibrary = [[ALAssetsLibrary alloc] init];
+ });
+ return assetsLibrary;
+}
+
++ (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, UIImage *image))callback
+{
+ if ([imageTag hasPrefix:@"assets-library"]) {
+ [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) {
+ if (asset) {
+ ALAssetRepresentation *representation = [asset defaultRepresentation];
+ ALAssetOrientation orientation = [representation orientation];
+ UIImage *image = [UIImage imageWithCGImage:[representation fullResolutionImage] scale:1.0f orientation:(UIImageOrientation)orientation];
+ callback(nil, image);
+ } else {
+ NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageTag];
+ NSError *error = errorWithMessage(errorText);
+ callback(error, nil);
+ }
+ } failureBlock:^(NSError *loadError) {
+ NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageTag, loadError];
+ NSError *error = errorWithMessage(errorText);
+ callback(error, nil);
+ }];
+ } else if ([imageTag hasPrefix:@"ph://"]) {
+ // Using PhotoKit for iOS 8+
+ // 'ph://' prefix is used by FBMediaKit to differentiate between assets-library. It is prepended to the local ID so that it
+ // is in the form of NSURL which is what assets-library is based on.
+ // This means if we use any FB standard photo picker, we will get this prefix =(
+ NSString *phAssetID = [imageTag substringFromIndex:[@"ph://" length]];
+ PHFetchResult *results = [PHAsset fetchAssetsWithLocalIdentifiers:@[phAssetID] options:nil];
+ if (results.count == 0) {
+ NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", phAssetID];
+ NSError *error = errorWithMessage(errorText);
+ callback(error, nil);
+ return;
+ }
+
+ PHAsset *asset = [results firstObject];
+ [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:nil resultHandler:^(UIImage *result, NSDictionary *info) {
+ if (result) {
+ callback(nil, result);
+ } else {
+ NSString *errorText = [NSString stringWithFormat:@"Failed to load PHAsset with local identifier %@ with no error message.", phAssetID];
+ NSError *error = errorWithMessage(errorText);
+ callback(error, nil);
+ return;
+ }
+ }];
+ } else if ([imageTag hasPrefix:@"http"]) {
+ NSURL *url = [NSURL URLWithString:imageTag];
+ if (!url) {
+ NSString *errorMessage = [NSString stringWithFormat:@"Invalid URL: %@", imageTag];
+ callback(errorWithMessage(errorMessage), nil);
+ return;
+ }
+ [[RCTImageDownloader sharedInstance] downloadDataForURL:url block:^(NSData *data, NSError *error) {
+ if (error) {
+ callback(error, nil);
+ } else {
+ callback(nil, [UIImage imageWithData:data]);
+ }
+ }];
+ } else {
+ NSString *errorMessage = [NSString stringWithFormat:@"Unrecognized tag protocol: %@", imageTag];
+ NSError *error = errorWithMessage(errorMessage);
+ callback(error, nil);
+ }
+}
+
+@end
diff --git a/Libraries/Image/RCTStaticImage.m b/Libraries/Image/RCTStaticImage.m
index b57b763ed..e8378fc72 100644
--- a/Libraries/Image/RCTStaticImage.m
+++ b/Libraries/Image/RCTStaticImage.m
@@ -24,7 +24,7 @@
// Apply trilinear filtering to smooth out mis-sized images
self.layer.minificationFilter = kCAFilterTrilinear;
self.layer.magnificationFilter = kCAFilterTrilinear;
-
+
super.image = image;
}
diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m
index b83d8c42b..ef60247f2 100644
--- a/Libraries/Image/RCTStaticImageManager.m
+++ b/Libraries/Image/RCTStaticImageManager.m
@@ -6,6 +6,7 @@
#import "RCTConvert.h"
#import "RCTGIFImage.h"
+#import "RCTImageLoader.h"
#import "RCTStaticImage.h"
@implementation RCTStaticImageManager
@@ -39,5 +40,19 @@ RCT_CUSTOM_VIEW_PROPERTY(tintColor, RCTStaticImage *)
view.tintColor = defaultView.tintColor;
}
}
+RCT_CUSTOM_VIEW_PROPERTY(imageTag, RCTStaticImage *)
+{
+ if (json) {
+ [RCTImageLoader loadImageWithTag:[RCTConvert NSString:json] callback:^(NSError *error, UIImage *image) {
+ if (error) {
+ RCTLogWarn(@"%@", error.localizedDescription);
+ } else {
+ view.image = image;
+ }
+ }];
+ } else {
+ view.image = defaultView.image;
+ }
+}
@end
diff --git a/Libraries/Utilities/groupByEveryN.js b/Libraries/Utilities/groupByEveryN.js
new file mode 100644
index 000000000..e85e58ed0
--- /dev/null
+++ b/Libraries/Utilities/groupByEveryN.js
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule groupByEveryN
+ */
+
+/**
+ * Useful method to split an array into groups of the same number of elements.
+ * You can use it to generate grids, rows, pages...
+ *
+ * If the input length is not a multiple of the count, it'll fill the last
+ * array with null so you can display a placeholder.
+ *
+ * Example:
+ * groupByEveryN([1, 2, 3, 4, 5], 3)
+ * => [[1, 2, 3], [4, 5, null]]
+ *
+ * groupByEveryN([1, 2, 3], 2).map(elems => {
+ * return {elems.map(elem => {elem})}
;
+ * })
+ */
+'use strict';
+
+function groupByEveryN(array, n) {
+ var result = [];
+ var temp = [];
+
+ for (var i = 0; i < array.length; ++i) {
+ if (i > 0 && i % n === 0) {
+ result.push(temp);
+ temp = [];
+ }
+ temp.push(array[i]);
+ }
+
+ if (temp.length > 0) {
+ while (temp.length !== n) {
+ temp.push(null);
+ }
+ result.push(temp);
+ }
+
+ return result;
+}
+
+module.exports = groupByEveryN;
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 7f7edaf72..4b2de062a 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -8,6 +8,7 @@
var ReactNative = {
...require('React'),
AppRegistry: require('AppRegistry'),
+ CameraRoll: require('CameraRoll'),
DatePickerIOS: require('DatePickerIOS'),
ExpandingText: require('ExpandingText'),
Image: require('Image'),