diff --git a/Examples/UIExplorer/AsyncStorageExample.js b/Examples/UIExplorer/AsyncStorageExample.js new file mode 100644 index 000000000..3a2579fa7 --- /dev/null +++ b/Examples/UIExplorer/AsyncStorageExample.js @@ -0,0 +1,103 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var React = require('react-native'); +var { + AsyncStorage, + PickerIOS, + Text, + View +} = React; +var PickerItemIOS = PickerIOS.Item; + +var STORAGE_KEY = '@AsyncStorageExample:key'; +var COLORS = ['red', 'orange', 'yellow', 'green', 'blue']; + +var BasicStorageExample = React.createClass({ + componentDidMount() { + AsyncStorage.getItem(STORAGE_KEY, (error, value) => { + if (error) { + this._appendMessage('AsyncStorage error: ' + error.message); + } else if (value !== null) { + this.setState({selectedValue: value}); + this._appendMessage('Recovered selection from disk: ' + value); + } else { + this._appendMessage('Initialized with no selection on disk.'); + } + }); + }, + getInitialState() { + return { + selectedValue: COLORS[0], + messages: [], + }; + }, + + render() { + var color = this.state.selectedValue; + return ( + + + {COLORS.map((value) => ( + + ))} + + + {'Selected: '} + + {this.state.selectedValue} + + + {' '} + + Press here to remove from storage. + + {' '} + Messages: + {this.state.messages.map((m) => {m})} + + ); + }, + + _onValueChange(selectedValue) { + this.setState({selectedValue}); + AsyncStorage.setItem(STORAGE_KEY, selectedValue, (error) => { + if (error) { + this._appendMessage('AsyncStorage error: ' + error.message); + } else { + this._appendMessage('Saved selection to disk: ' + selectedValue); + } + }); + }, + + _removeStorage() { + AsyncStorage.removeItem(STORAGE_KEY, (error) => { + if (error) { + this._appendMessage('AsyncStorage error: ' + error.message); + } else { + this._appendMessage('Selection removed from disk.'); + } + }); + }, + + _appendMessage(message) { + this.setState({messages: this.state.messages.concat(message)}); + }, +}); + +exports.title = 'AsyncStorage'; +exports.description = 'Asynchronous local disk storage.'; +exports.examples = [ + { + title: 'Basics - getItem, setItem, removeItem', + render() { return ; } + }, +]; diff --git a/Examples/UIExplorer/UIExplorerApp.js b/Examples/UIExplorer/UIExplorerApp.js index 7c73d4ee3..95d02347e 100644 --- a/Examples/UIExplorer/UIExplorerApp.js +++ b/Examples/UIExplorer/UIExplorerApp.js @@ -16,6 +16,7 @@ var { var UIExplorerApp = React.createClass({ + render: function() { return ( + tintColor='#008888' + /> ); } }); diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 63c983838..e0cfee985 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -37,6 +37,7 @@ var EXAMPLES = [ require('./TabBarExample'), require('./SwitchExample'), require('./SliderExample'), + require('./AsyncStorageExample'), require('./CameraRollExample.ios'), require('./MapViewExample'), require('./AdSupportIOSExample'), diff --git a/Libraries/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.ios.js new file mode 100644 index 000000000..a29288f36 --- /dev/null +++ b/Libraries/Storage/AsyncStorage.ios.js @@ -0,0 +1,193 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AsyncStorage + * @flow-weak + */ +'use strict'; + +var NativeModules = require('NativeModulesDeprecated'); +var RKAsyncLocalStorage = NativeModules.RKAsyncLocalStorage; +var RKAsyncRocksDBStorage = NativeModules.RKAsyncRocksDBStorage; + +// We use RocksDB if available. +var RKAsyncStorage = RKAsyncRocksDBStorage || RKAsyncLocalStorage; + +/** + * AsyncStorage is a simple, asynchronous, persistent, global, key-value storage + * system. It should be used instead of LocalStorage. + * + * It is recommended that you use an abstraction on top of AsyncStorage instead + * of AsyncStorage directly for anything more than light usage since it + * operates globally. + * + * This JS code is a simple facad over the native iOS implementation to provide + * a clear JS API, real Error objects, and simple non-multi functions. + */ +var AsyncStorage = { + /** + * Fetches `key` and passes the result to `callback`, along with an `Error` if + * there is any. + */ + getItem: function( + key: string, + callback: (error: ?Error, result: ?string) => void + ): void { + RKAsyncStorage.multiGet([key], function(errors, result) { + // Unpack result to get value from [[key,value]] + var value = (result && result[0] && result[0][1]) ? result[0][1] : null; + callback((errors && convertError(errors[0])) || null, value); + }); + }, + + /** + * Sets `value` for `key` and calls `callback` on completion, along with an + * `Error` if there is any. + */ + setItem: function( + key: string, + value: string, + callback: ?(error: ?Error) => void + ): void { + RKAsyncStorage.multiSet([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + }); + }, + + removeItem: function( + key: string, + callback: ?(error: ?Error) => void + ): void { + RKAsyncStorage.multiRemove([key], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + }); + }, + + /** + * Merges existing value with input value, assuming they are stringified json. + * + * Not supported by all native implementations. + */ + mergeItem: function( + key: string, + value: string, + callback: ?(error: ?Error) => void + ): void { + RKAsyncStorage.multiMerge([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + }); + }, + + /** + * Erases *all* AsyncStorage for all clients, libraries, etc. You probably + * don't want to call this - use removeItem or multiRemove to clear only your + * own keys instead. + */ + clear: function(callback: ?(error: ?Error) => void) { + RKAsyncStorage.clear(function(error) { + callback && callback(convertError(error)); + }); + }, + + /** + * Gets *all* keys known to the system, for all callers, libraries, etc. + */ + getAllKeys: function(callback: (error: ?Error) => void) { + RKAsyncStorage.getAllKeys(function(error, keys) { + callback(convertError(error), keys); + }); + }, + + /** + * The following batched functions are useful for executing a lot of + * operations at once, allowing for native optimizations and provide the + * convenience of a single callback after all operations are complete. + * + * These functions return arrays of errors, potentially one for every key. + * For key-specific errors, the Error object will have a key property to + * indicate which key caused the error. + */ + + /** + * multiGet invokes callback with an array of key-value pair arrays that + * matches the input format of multiSet. + * + * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + */ + multiGet: function( + keys: Array, + callback: (errors: ?Array, result: ?Array>) => void + ): void { + RKAsyncStorage.multiGet(keys, function(errors, result) { + callback( + (errors && errors.map((error) => convertError(error))) || null, + result + ); + }); + }, + + /** + * multiSet and multiMerge take arrays of key-value array pairs that match + * the output of multiGet, e.g. + * + * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + */ + multiSet: function( + keyValuePairs: Array>, + callback: ?(errors: ?Array) => void + ): void { + RKAsyncStorage.multiSet(keyValuePairs, function(errors) { + callback && callback( + (errors && errors.map((error) => convertError(error))) || null + ); + }); + }, + + /** + * Delete all the keys in the `keys` array. + */ + multiRemove: function( + keys: Array, + callback: ?(errors: ?Array) => void + ): void { + RKAsyncStorage.multiRemove(keys, function(errors) { + callback && callback( + (errors && errors.map((error) => convertError(error))) || null + ); + }); + }, + + /** + * Merges existing values with input values, assuming they are stringified + * json. + * + * Not supported by all native implementations. + */ + multiMerge: function( + keyValuePairs: Array>, + callback: ?(errors: ?Array) => void + ): void { + RKAsyncStorage.multiMerge(keyValuePairs, function(errors) { + callback && callback( + (errors && errors.map((error) => convertError(error))) || null + ); + }); + }, +}; + +// Not all native implementations support merge. +if (!RKAsyncStorage.multiMerge) { + delete AsyncStorage.mergeItem; + delete AsyncStorage.multiMerge; +} + +function convertError(error) { + if (!error) { + return null; + } + var out = new Error(error.message); + out.key = error.key; // flow doesn't like this :( + return out; +} + +module.exports = AsyncStorage; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 4d4eee73d..c673e4e0c 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -8,20 +8,21 @@ var ReactNative = { ...require('React'), Animation: require('Animation'), + ActivityIndicatorIOS: require('ActivityIndicatorIOS'), AppRegistry: require('AppRegistry'), + AsyncStorage: require('AsyncStorage'), CameraRoll: require('CameraRoll'), DatePickerIOS: require('DatePickerIOS'), ExpandingText: require('ExpandingText'), - MapView: require('MapView'), Image: require('Image'), LayoutAnimation: require('LayoutAnimation'), ListView: require('ListView'), ListViewDataSource: require('ListViewDataSource'), + MapView: require('MapView'), NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), PixelRatio: require('PixelRatio'), ScrollView: require('ScrollView'), - ActivityIndicatorIOS: require('ActivityIndicatorIOS'), Slider: require('Slider'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), diff --git a/ReactKit/Base/RCTUtils.h b/ReactKit/Base/RCTUtils.h index de203e4ae..adf35cb9b 100644 --- a/ReactKit/Base/RCTUtils.h +++ b/ReactKit/Base/RCTUtils.h @@ -41,6 +41,11 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); // Enumerate all classes that conform to NSObject protocol void RCTEnumerateClasses(void (^block)(Class cls)); +// Creates a standardized error object +// TODO(#6472857): create NSErrors and automatically convert them over the bridge. +NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData); +NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData); + #ifdef __cplusplus } #endif diff --git a/ReactKit/Base/RCTUtils.m b/ReactKit/Base/RCTUtils.m index 40007a69b..217368e17 100644 --- a/ReactKit/Base/RCTUtils.m +++ b/ReactKit/Base/RCTUtils.m @@ -194,3 +194,22 @@ void RCTEnumerateClasses(void (^block)(Class cls)) } } } + +NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData) +{ + if (toStringify) { + message = [NSString stringWithFormat:@"%@%@", message, toStringify]; + } + NSMutableDictionary *error = [@{@"message": message} mutableCopy]; + if (extraData) { + [error addEntriesFromDictionary:extraData]; + } + return error; +} + +NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData) +{ + id error = RCTMakeError(message, toStringify, extraData); + RCTLogError(@"\nError: %@", error); + return error; +} diff --git a/ReactKit/Modules/RCTAsyncLocalStorage.h b/ReactKit/Modules/RCTAsyncLocalStorage.h new file mode 100644 index 000000000..54a320749 --- /dev/null +++ b/ReactKit/Modules/RCTAsyncLocalStorage.h @@ -0,0 +1,24 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +/** + * A simple, asynchronous, persistent, key-value storage system designed as a + * backend to the AsyncStorage JS module, which is modeled after LocalStorage. + * + * Current implementation stores small values in serialized dictionary and + * larger values in separate files. Since we use a serial file queue + * `RKFileQueue`, reading/writing from multiple threads should be perceived as + * being atomic, unless someone bypasses the `RCTAsyncLocalStorage` API. + * + * Keys and values must always be strings or an error is returned. + */ +@interface RCTAsyncLocalStorage : NSObject + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)clear:(RCTResponseSenderBlock)callback; +- (void)getAllKeys:(RCTResponseSenderBlock)callback; + +@end diff --git a/ReactKit/Modules/RCTAsyncLocalStorage.m b/ReactKit/Modules/RCTAsyncLocalStorage.m new file mode 100644 index 000000000..de8b7989e --- /dev/null +++ b/ReactKit/Modules/RCTAsyncLocalStorage.m @@ -0,0 +1,292 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTAsyncLocalStorage.h" + +#import + +#import +#import + +#import "RCTLog.h" +#import "RCTUtils.h" + +static NSString *const kStorageDir = @"RCTAsyncLocalStorage_V1"; +static NSString *const kManifestFilename = @"manifest.json"; +static const NSUInteger kInlineValueThreshold = 100; + +#pragma mark - Static helper functions + +static id RCTErrorForKey(NSString *key) +{ + if (![key isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); + } else if (key.length < 1) { + return RCTMakeAndLogError(@"Invalid key - must be at least one character. Key: ", key, @{@"key": key}); + } else { + return nil; + } +} + +static void RCTAppendError(id error, NSMutableArray **errors) +{ + if (error && errors) { + if (!*errors) { + *errors = [NSMutableArray new]; + } + [*errors addObject:error]; + } +} + +static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +{ + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + NSError *error; + NSStringEncoding encoding; + NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; + if (error) { + *errorOut = RCTMakeError(@"Failed to read storage file.", error, @{@"key": key}); + } else if (encoding != NSUTF8StringEncoding) { + *errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), @{@"key": key}); + } else { + return entryString; + } + } + return nil; +} + +static dispatch_queue_t RCTFileQueue(void) +{ + static dispatch_queue_t fileQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // All JS is single threaded, so a serial queue is our only option. + fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(fileQueue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + }); + + return fileQueue; +} + +#pragma mark - RCTAsyncLocalStorage + +@implementation RCTAsyncLocalStorage +{ + BOOL _haveSetup; + // The manifest is a dictionary of all keys with small values inlined. Null values indicate values that are stored + // in separate files (as opposed to nil values which don't exist). The manifest is read off disk at startup, and + // written to disk after all mutations. + NSMutableDictionary *_manifest; + NSString *_manifestPath; + NSString *_storageDirectory; +} + +- (NSString *)_filePathForKey:(NSString *)key +{ + NSString *safeFileName = RCTMD5Hash(key); + return [_storageDirectory stringByAppendingPathComponent:safeFileName]; +} + +- (id)_ensureSetup +{ + if (_haveSetup) { + return nil; + } + NSString *documentDirectory = + [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; + _storageDirectory = [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; + NSDictionary *errorOut; + NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + if (error) { + RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); + _manifest = [NSMutableDictionary new]; + } + _haveSetup = YES; + return nil; +} + +- (id)_writeManifest:(NSMutableArray **)errors +{ + NSError *error; + NSString *serialized = RCTJSONStringify(_manifest, &error); + [serialized writeToFile:_manifestPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + id errorOut; + if (error) { + errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); + RCTAppendError(errorOut, errors); + } + return errorOut; +} + +- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +{ + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + if (value == [NSNull null]) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, &errorOut); + } + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (id)_writeEntry:(NSArray *)entry +{ + if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) { + return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); + } + if (![entry[1] isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], entry[0]); + } + NSString *key = entry[0]; + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + NSString *value = entry[1]; + NSString *filePath = [self _filePathForKey:key]; + NSError *error; + if (value.length <= kInlineValueThreshold) { + if (_manifest[key] && _manifest[key] != [NSNull null]) { + // If the value already existed but wasn't inlined, remove the old file. + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } + _manifest[key] = value; + return nil; + } + [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); + } else { + _manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data. + } + return errorOut; +} + +#pragma mark - Exported JS Functions + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + if (!callback) { + RCTLogError(@"Called getItem without a callback."); + return; + } + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut], [NSNull null]]); + return; + } + NSMutableArray *errors; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + for (NSString *key in keys) { + id keyError = [self _appendItemForKey:key toArray:result]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + callback(@[errors ?: [NSNull null], result]); + }); +} + +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + id keyError = [self _writeEntry:entry]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSString *key in keys) { + id keyError = RCTErrorForKey(key); + if (!keyError) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [_manifest removeObjectForKey:key]; + } + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)clear:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (!errorOut) { + NSError *error; + for (NSString *key in _manifest) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + } + [_manifest removeAllObjects]; + errorOut = [self _writeManifest:nil]; + } + if (callback) { + callback(@[errorOut ?: [NSNull null]]); + } + }); +} + +- (void)getAllKeys:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, [NSNull null]]); + } else { + callback(@[[NSNull null], [_manifest allKeys]]); + } + }); +} + +@end diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index fc8e49ac3..496c2c540 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A131AAE854800E7D092 /* RCTPicker.m */; }; 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */; }; + 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; @@ -147,6 +148,8 @@ 58114A151AAE854800E7D092 /* RCTPickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPickerManager.m; sourceTree = ""; }; 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePickerManager.m; sourceTree = ""; }; 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = ""; }; + 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAsyncLocalStorage.m; sourceTree = ""; }; + 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAsyncLocalStorage.h; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; @@ -215,6 +218,8 @@ 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */, 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */, 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */, + 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */, + 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */, 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */, 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */, 5F5F0D971A9E456B001279FA /* RCTLocationObserver.h */, @@ -439,6 +444,7 @@ 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, + 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, diff --git a/ReactKit/Views/RCTPicker.h b/ReactKit/Views/RCTPicker.h index cbc55c7f3..8b5b3c864 100644 --- a/ReactKit/Views/RCTPicker.h +++ b/ReactKit/Views/RCTPicker.h @@ -8,4 +8,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; +@property (nonatomic, copy) NSArray *items; +@property (nonatomic, assign) NSInteger selectedIndex; + @end