From e3ceb411d78022c04cf6e86fd6b393137c292845 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Sat, 9 Mar 2019 17:19:47 +0100 Subject: [PATCH 01/16] feat: Allow brownfield iOS apps to handle storage. --- .../project.pbxproj | 26 +++- example/ios/AsyncStorageExample/AppDelegate.h | 4 +- example/ios/AsyncStorageExample/AppDelegate.m | 90 ++++++++++++- ios/RNCAsyncStorage.h | 4 +- ios/RNCAsyncStorage.m | 119 ++++++++++++++++-- ios/RNCAsyncStorage.xcodeproj/project.pbxproj | 21 +++- ios/RNCAsyncStorageDelegate.h | 37 ++++++ 7 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 ios/RNCAsyncStorageDelegate.h diff --git a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj index 37033d1a..c90aa9b5 100644 --- a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj +++ b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj @@ -84,6 +84,13 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; + 1990B95122398FC4009E5EA1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 58B511DA1A9E6C8500147676; + remoteInfo = RNCAsyncStorage; + }; 2D16E6711FA4F8DC00B85C8A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; @@ -329,11 +336,9 @@ buildActionMask = 2147483647; files = ( 3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */, - ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */, - ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, - 11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */, - 146834051AC3E58100842450 /* libReact.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, + 11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */, + ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, @@ -342,6 +347,8 @@ 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, + 146834051AC3E58100842450 /* libReact.a in Frameworks */, + ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -557,6 +564,7 @@ buildRules = ( ); dependencies = ( + 1990B95222398FC4009E5EA1 /* PBXTargetDependency */, ); name = AsyncStorageExample; productName = "Hello World"; @@ -938,7 +946,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh"; + shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -954,6 +962,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 1990B95222398FC4009E5EA1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = RNCAsyncStorage; + targetProxy = 1990B95122398FC4009E5EA1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; diff --git a/example/ios/AsyncStorageExample/AppDelegate.h b/example/ios/AsyncStorageExample/AppDelegate.h index 4b5644f2..1d413e36 100644 --- a/example/ios/AsyncStorageExample/AppDelegate.h +++ b/example/ios/AsyncStorageExample/AppDelegate.h @@ -7,7 +7,9 @@ #import -@interface AppDelegate : UIResponder +#import + +@interface AppDelegate : UIResponder @property (nonatomic, strong) UIWindow *window; diff --git a/example/ios/AsyncStorageExample/AppDelegate.m b/example/ios/AsyncStorageExample/AppDelegate.m index 18f05547..b3f825d3 100644 --- a/example/ios/AsyncStorageExample/AppDelegate.m +++ b/example/ios/AsyncStorageExample/AppDelegate.m @@ -7,13 +7,22 @@ #import "AppDelegate.h" +#import #import +#import #import -@implementation AppDelegate +@implementation AppDelegate { + NSMutableDictionary *_memoryStorage; +} - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didLoadJavaScript:) + name:RCTJavaScriptDidLoadNotification + object:nil]; + NSURL *jsCodeLocation; jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"example/index" fallbackResource:nil]; @@ -32,4 +41,83 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } +- (void)addDevMenuItemsForBridge:(RCTBridge *)bridge +{ + _memoryStorage = [NSMutableDictionary dictionary]; + + __weak AppDelegate *weakSelf = self; + RCTDevMenuItem *delegateToggle = [RCTDevMenuItem + buttonItemWithTitleBlock:^NSString * { + RNCAsyncStorage *asyncStorage = [bridge moduleForClass:[RNCAsyncStorage class]]; + return asyncStorage.delegate == nil ? @"Set AsyncStorage Delegate" + : @"Unset AsyncStorage Delegate"; + } + handler:^{ + RNCAsyncStorage *asyncStorage = [bridge moduleForClass:[RNCAsyncStorage class]]; + asyncStorage.delegate = asyncStorage.delegate == nil ? weakSelf : nil; + }]; + [bridge.devMenu addItem:delegateToggle]; +} + +- (void)didLoadJavaScript:(NSNotification *)note +{ + RCTBridge *bridge = note.userInfo[@"bridge"]; + if (bridge == nil) { + return; + } + + [self addDevMenuItemsForBridge:bridge]; +} + +#pragma mark - RNCAsyncStorageDelegate + +- (void)allKeys:(nonnull RNCAsyncStorageResultCallback)completion +{ + completion(_memoryStorage.allKeys); +} + +- (void)mergeValues:(nonnull NSArray *)values + forKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)block +{ +} + +- (void)removeAllValues:(nonnull RNCAsyncStorageCompletion)completion +{ + [_memoryStorage removeAllObjects]; + completion(nil); +} + +- (void)removeValuesForKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + for (NSString *key in keys) { + [_memoryStorage removeObjectForKey:key]; + } + completion(@[]); +} + +- (void)setValues:(nonnull NSArray *)values + forKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + for (NSUInteger i = 0; i < values.count; ++i) { + NSString *value = values[i]; + NSString *key = keys[i]; + [_memoryStorage setObject:value forKey:key]; + } + completion(@[]); +} + +- (void)valuesForKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count]; + for (NSString *key in keys) { + NSString *value = _memoryStorage[key]; + [values addObject:value == nil ? [NSNull null] : value]; + } + completion(values); +} + @end diff --git a/ios/RNCAsyncStorage.h b/ios/RNCAsyncStorage.h index cfb8c7a1..36c4b7d9 100644 --- a/ios/RNCAsyncStorage.h +++ b/ios/RNCAsyncStorage.h @@ -7,6 +7,7 @@ #import #import +#import /** * A simple, asynchronous, persistent, key-value storage system designed as a @@ -21,6 +22,8 @@ */ @interface RNCAsyncStorage : NSObject +@property (nonatomic, weak, nullable) id delegate; + @property (nonatomic, assign) BOOL clearOnInvalidate; @property (nonatomic, readonly, getter=isValid) BOOL valid; @@ -37,5 +40,4 @@ // Add multiple key value pairs to the cache. - (void)multiSet:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback; - @end diff --git a/ios/RNCAsyncStorage.m b/ios/RNCAsyncStorage.m index 308bf527..354fa667 100644 --- a/ios/RNCAsyncStorage.m +++ b/ios/RNCAsyncStorage.m @@ -43,6 +43,18 @@ static void RCTAppendError(NSDictionary *error, NSMutableArray * } } +static NSArray *RCTMakeErrors(NSArray> *results) { + NSMutableArray *errors; + for (id object in results) { + if ([object isKindOfClass:[NSError class]]) { + NSError *error = (NSError *)object; + NSDictionary *keyError = RCTMakeError(error.localizedDescription, error, nil); + RCTAppendError(keyError, &errors); + } + } + return errors; +} + static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) { if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { @@ -329,30 +341,77 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL return errorOut; } +- (void)_multiGet:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback + getter:(NSString *(^)(NSUInteger i, NSString *key, NSDictionary **errorOut))getValue +{ + NSMutableArray *errors; + NSMutableArray *> *result = [NSMutableArray arrayWithCapacity:keys.count]; + for (NSUInteger i = 0; i < keys.count; ++i) { + NSString *key = keys[i]; + id keyError; + id value = getValue(i, key, &keyError); + [result addObject:@[key, RCTNullIfNil(value)]]; + RCTAppendError(keyError, &errors); + } + callback(@[RCTNullIfNil(errors), result]); +} + #pragma mark - Exported JS Functions RCT_EXPORT_METHOD(multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { + if (self.delegate != nil) { + [self.delegate valuesForKeys:keys completion:^(NSArray> *valuesOrErrors) { + [self _multiGet:keys + callback:callback + getter:^NSString *(NSUInteger i, NSString *key, NSDictionary **errorOut) { + id valueOrError = valuesOrErrors[i]; + if ([valueOrError isKindOfClass:[NSError class]]) { + NSError *error = (NSError *)valueOrError; + NSDictionary *extraData = @{@"key": RCTNullIfNil(key)}; + *errorOut = RCTMakeError(error.localizedDescription, error, extraData); + return nil; + } else { + return [valueOrError isKindOfClass:[NSString class]] + ? (NSString *)valueOrError + : nil; + } + }]; + }]; + return; + } + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut], (id)kCFNull]); return; } - NSMutableArray *errors; - NSMutableArray *> *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; - for (NSString *key in keys) { - id keyError; - id value = [self _getValueForKey:key errorOut:&keyError]; - [result addObject:@[key, RCTNullIfNil(value)]]; - RCTAppendError(keyError, &errors); - } - callback(@[RCTNullIfNil(errors), result]); + [self _multiGet:keys + callback:callback + getter:^(NSUInteger i, NSString *key, NSDictionary **errorOut) { + return [self _getValueForKey:key errorOut:errorOut]; + }]; } RCT_EXPORT_METHOD(multiSet:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback) { + if (self.delegate != nil) { + NSMutableArray *keys = [NSMutableArray arrayWithCapacity:kvPairs.count]; + NSMutableArray *values = [NSMutableArray arrayWithCapacity:kvPairs.count]; + for (NSArray *entry in kvPairs) { + [keys addObject:entry[0]]; + [values addObject:entry[1]]; + } + [self.delegate setValues:values forKeys:keys completion:^(NSArray> *results) { + NSArray *errors = RCTMakeErrors(results); + callback(@[RCTNullIfNil(errors)]); + }]; + return; + } + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); @@ -373,6 +432,20 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL RCT_EXPORT_METHOD(multiMerge:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback) { + if (self.delegate != nil) { + NSMutableArray *keys = [NSMutableArray arrayWithCapacity:kvPairs.count]; + NSMutableArray *values = [NSMutableArray arrayWithCapacity:kvPairs.count]; + for (NSArray *entry in kvPairs) { + [keys addObject:entry[0]]; + [values addObject:entry[1]]; + } + [self.delegate mergeValues:values forKeys:keys completion:^(NSArray> *results) { + NSArray *errors = RCTMakeErrors(results); + callback(@[RCTNullIfNil(errors)]); + }]; + return; + } + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); @@ -407,8 +480,16 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL } RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys - callback:(RCTResponseSenderBlock)callback) + callback:(RCTResponseSenderBlock)callback) { + if (self.delegate != nil) { + [self.delegate removeValuesForKeys:keys completion:^(NSArray> *results) { + NSArray *errors = RCTMakeErrors(results); + callback(@[RCTNullIfNil(errors)]); + }]; + return; + } + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); @@ -439,6 +520,17 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { + if (self.delegate != nil) { + [self.delegate removeAllValues:^(NSError *error) { + NSDictionary *result = nil; + if (error != nil) { + result = RCTMakeError(error.localizedDescription, error, nil); + } + callback(@[RCTNullIfNil(result)]); + }]; + return; + } + [_manifest removeAllObjects]; [RCTGetCache() removeAllObjects]; NSDictionary *error = RCTDeleteStorageDirectory(); @@ -447,6 +539,13 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { + if (self.delegate != nil) { + [self.delegate allKeys:^(NSArray> *keys) { + callback(@[(id)kCFNull, keys]); + }]; + return; + } + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[errorOut, (id)kCFNull]); diff --git a/ios/RNCAsyncStorage.xcodeproj/project.pbxproj b/ios/RNCAsyncStorage.xcodeproj/project.pbxproj index 3bd630c1..c4f0d74f 100644 --- a/ios/RNCAsyncStorage.xcodeproj/project.pbxproj +++ b/ios/RNCAsyncStorage.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1990B97A223993B0009E5EA1 /* RNCAsyncStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = B3E7B5881CC2AC0600A0062D /* RNCAsyncStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1990B97B223993B0009E5EA1 /* RNCAsyncStorageDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1990B9402233FE3A009E5EA1 /* RNCAsyncStorageDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; B3E7B58A1CC2AC0600A0062D /* RNCAsyncStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNCAsyncStorage.m */; }; /* End PBXBuildFile section */ @@ -17,6 +19,8 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( + 1990B97A223993B0009E5EA1 /* RNCAsyncStorage.h in CopyFiles */, + 1990B97B223993B0009E5EA1 /* RNCAsyncStorageDelegate.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -24,6 +28,7 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libRNCAsyncStorage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCAsyncStorage.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 1990B9402233FE3A009E5EA1 /* RNCAsyncStorageDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCAsyncStorageDelegate.h; sourceTree = ""; }; B3E7B5881CC2AC0600A0062D /* RNCAsyncStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCAsyncStorage.h; sourceTree = ""; }; B3E7B5891CC2AC0600A0062D /* RNCAsyncStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCAsyncStorage.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -52,17 +57,31 @@ children = ( B3E7B5881CC2AC0600A0062D /* RNCAsyncStorage.h */, B3E7B5891CC2AC0600A0062D /* RNCAsyncStorage.m */, + 1990B9402233FE3A009E5EA1 /* RNCAsyncStorageDelegate.h */, 134814211AA4EA7D00B7C361 /* Products */, ); sourceTree = ""; }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + 19F94B1D2239A948006921A9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1990B97A223993B0009E5EA1 /* RNCAsyncStorage.h in Headers */, + 1990B97B223993B0009E5EA1 /* RNCAsyncStorageDelegate.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ 58B511DA1A9E6C8500147676 /* RNCAsyncStorage */ = { isa = PBXNativeTarget; buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNCAsyncStorage" */; buildPhases = ( + 19F94B1D2239A948006921A9 /* Headers */, 58B511D71A9E6C8500147676 /* Sources */, 58B511D81A9E6C8500147676 /* Frameworks */, 58B511D91A9E6C8500147676 /* CopyFiles */, @@ -204,7 +223,7 @@ isa = XCBuildConfiguration; buildSettings = { HEADER_SEARCH_PATHS = ( - "$(inherited)", + "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../../React/**", "$(SRCROOT)/../../react-native/React/**", diff --git a/ios/RNCAsyncStorageDelegate.h b/ios/RNCAsyncStorageDelegate.h new file mode 100644 index 00000000..9f481665 --- /dev/null +++ b/ios/RNCAsyncStorageDelegate.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^RNCAsyncStorageCompletion)(NSError *_Nullable error); +typedef void (^RNCAsyncStorageResultCallback)(NSArray> * valuesOrErrors); + +@protocol RNCAsyncStorageDelegate + +- (void)allKeys:(RNCAsyncStorageResultCallback)block; + +- (void)mergeValues:(NSArray *)values + forKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; + +- (void)removeAllValues:(RNCAsyncStorageCompletion)block; + +- (void)removeValuesForKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; + +- (void)setValues:(NSArray *)values + forKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; + +- (void)valuesForKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; + +@end + +NS_ASSUME_NONNULL_END From efca31714da30ed80431e8c1b3c033f8ac174e5b Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Tue, 26 Mar 2019 09:27:19 +0100 Subject: [PATCH 02/16] Allow passthrough. --- ios/RNCAsyncStorage.m | 30 +++++++++++++++++++++++++----- ios/RNCAsyncStorageDelegate.h | 4 ++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ios/RNCAsyncStorage.m b/ios/RNCAsyncStorage.m index 354fa667..d258f554 100644 --- a/ios/RNCAsyncStorage.m +++ b/ios/RNCAsyncStorage.m @@ -357,6 +357,11 @@ - (void)_multiGet:(NSArray *)keys callback(@[RCTNullIfNil(errors), result]); } +- (BOOL)_passthroughDelegate +{ + return [self.delegate respondsToSelector:@selector(isPassthrough)] && self.delegate.isPassthrough; +} + #pragma mark - Exported JS Functions RCT_EXPORT_METHOD(multiGet:(NSArray *)keys @@ -380,7 +385,10 @@ - (void)_multiGet:(NSArray *)keys } }]; }]; - return; + + if (![self _passthroughDelegate]) { + return; + } } NSDictionary *errorOut = [self _ensureSetup]; @@ -409,7 +417,10 @@ - (void)_multiGet:(NSArray *)keys NSArray *errors = RCTMakeErrors(results); callback(@[RCTNullIfNil(errors)]); }]; - return; + + if (![self _passthroughDelegate]) { + return; + } } NSDictionary *errorOut = [self _ensureSetup]; @@ -443,7 +454,10 @@ - (void)_multiGet:(NSArray *)keys NSArray *errors = RCTMakeErrors(results); callback(@[RCTNullIfNil(errors)]); }]; - return; + + if (![self _passthroughDelegate]) { + return; + } } NSDictionary *errorOut = [self _ensureSetup]; @@ -487,7 +501,10 @@ - (void)_multiGet:(NSArray *)keys NSArray *errors = RCTMakeErrors(results); callback(@[RCTNullIfNil(errors)]); }]; - return; + + if (![self _passthroughDelegate]) { + return; + } } NSDictionary *errorOut = [self _ensureSetup]; @@ -543,7 +560,10 @@ - (void)_multiGet:(NSArray *)keys [self.delegate allKeys:^(NSArray> *keys) { callback(@[(id)kCFNull, keys]); }]; - return; + + if (![self _passthroughDelegate]) { + return; + } } NSDictionary *errorOut = [self _ensureSetup]; diff --git a/ios/RNCAsyncStorageDelegate.h b/ios/RNCAsyncStorageDelegate.h index 9f481665..7312c17f 100644 --- a/ios/RNCAsyncStorageDelegate.h +++ b/ios/RNCAsyncStorageDelegate.h @@ -32,6 +32,10 @@ typedef void (^RNCAsyncStorageResultCallback)(NSArray> * valuesOrEr - (void)valuesForKeys:(NSArray *)keys completion:(RNCAsyncStorageResultCallback)block; +@optional + +@property (nonatomic, readonly, getter=isPassthrough) BOOL passthrough; + @end NS_ASSUME_NONNULL_END From fff6459cc750531c46310769fa7021bd6de66a17 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Tue, 26 Mar 2019 23:37:39 +0100 Subject: [PATCH 03/16] Added docs. --- README.md | 4 +- docs/AdvancedUsage.md | 82 +++++++++++++++++++++++++++++++++++ ios/RNCAsyncStorageDelegate.h | 33 ++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 docs/AdvancedUsage.md diff --git a/README.md b/README.md index 4834268c..fd6693e2 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ getData = async () => { ``` -See docs for [api and more examples.](docs/API.md) +See docs for [api and more examples](docs/API.md), and [some advanced usage](docs/AdvancedUsage.md). ## Writing tests @@ -66,4 +66,4 @@ See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out. ## License -MIT \ No newline at end of file +MIT diff --git a/docs/AdvancedUsage.md b/docs/AdvancedUsage.md new file mode 100644 index 00000000..f16c4f68 --- /dev/null +++ b/docs/AdvancedUsage.md @@ -0,0 +1,82 @@ +# Advanced Usage + +## Integrating with Existing Storage Solutions in Hybrid Apps + +### iOS + +On iOS, AsyncStorage can be controlled by the hosting app via the delegate on +`RNCAsyncStorage`: + +```objc +RNCAsyncStorage *asyncStorage = [bridge moduleForClass:[RNCAsyncStorage class]]; +asyncStorage.delegate = self; +``` + +The delegate must conform to the protocol `RNCAsyncStorageDelegate`: + +```objc +- (void)allKeys:(RNCAsyncStorageResultCallback)block; +``` + +Returns all keys currently stored. If none, an empty array is returned. +Called by `getAllKeys` in JS. + +
+ +```objc +- (void)mergeValues:(NSArray *)values + forKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; +``` + +Merges values with the corresponding values stored at specified keys. +Called by `mergeItem` and `multiMerge` in JS. + +
+ +```objc +- (void)removeAllValues:(RNCAsyncStorageCompletion)block; +``` + +Removes all values from the store. Called by `clear` in JS. + +
+ +```objc +- (void)removeValuesForKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; +``` + +Removes all values associated with specified keys. +Called by `removeItem` and `multiRemove` in JS. + +
+ +```objc +- (void)setValues:(NSArray *)values + forKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; +``` + +Sets specified key-value pairs. Called by `setItem` and `multiSet` in JS. + +
+ +```objc +- (void)valuesForKeys:(NSArray *)keys + completion:(RNCAsyncStorageResultCallback)block; +``` + +Returns values associated with specified keys. +Called by `getItem` and `multiGet` in JS. + +
+ +```objc +@optional +@property (nonatomic, readonly, getter=isPassthrough) BOOL passthrough; +``` + +**Optional:** Returns whether the delegate should be treated as a passthrough. +This is useful for monitoring storage usage among other things. Returns `NO` by +default. diff --git a/ios/RNCAsyncStorageDelegate.h b/ios/RNCAsyncStorageDelegate.h index 7312c17f..84e129e1 100644 --- a/ios/RNCAsyncStorageDelegate.h +++ b/ios/RNCAsyncStorageDelegate.h @@ -14,26 +14,59 @@ typedef void (^RNCAsyncStorageResultCallback)(NSArray> * valuesOrEr @protocol RNCAsyncStorageDelegate +/*! + * Returns all keys currently stored. If none, an empty array is returned. + * @param block Block to call with result. + */ - (void)allKeys:(RNCAsyncStorageResultCallback)block; +/*! + * Merges values with the corresponding values stored at specified keys. + * @param values Values to merge. + * @param keys Keys to the values that should be merged with. + * @param block Block to call with merged result. + */ - (void)mergeValues:(NSArray *)values forKeys:(NSArray *)keys completion:(RNCAsyncStorageResultCallback)block; +/*! + * Removes all values from the store. + * @param block Block to call with result. + */ - (void)removeAllValues:(RNCAsyncStorageCompletion)block; +/*! + * Removes all values associated with specified keys. + * @param keys Keys of values to remove. + * @param block Block to call with result. + */ - (void)removeValuesForKeys:(NSArray *)keys completion:(RNCAsyncStorageResultCallback)block; +/*! + * Sets specified key-value pairs. + * @param values Values to set. + * @param keys Keys of specified values to set. + * @param block Block to call with result. + */ - (void)setValues:(NSArray *)values forKeys:(NSArray *)keys completion:(RNCAsyncStorageResultCallback)block; +/*! + * Returns values associated with specified keys. + * @param keys Keys of values to return. + * @param block Block to call with result. + */ - (void)valuesForKeys:(NSArray *)keys completion:(RNCAsyncStorageResultCallback)block; @optional +/*! + * Returns whether the delegate should be treated as a passthrough. + */ @property (nonatomic, readonly, getter=isPassthrough) BOOL passthrough; @end From a6af9d0f937be4755abc686f3d693168bf7fbdb6 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 27 Mar 2019 10:14:54 +0100 Subject: [PATCH 04/16] Added tests. --- example/e2e/asyncstorage.e2e.js | 77 +++++++++++++++++++ example/ios/AsyncStorageExample/AppDelegate.m | 23 ++++++ example/ios/AsyncStorageExample/Info.plist | 42 ++++++---- 3 files changed, 126 insertions(+), 16 deletions(-) diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index 5d1b4f50..59b304bb 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -29,12 +29,17 @@ describe('Async Storage', () => { }); describe('get / set / clear item test', () => { + beforeAll(async () => { + await device.openURL({ url: 'rnc-asyncstorage://unset-delegate' }); + }); + it('should be visible', async () => { await test_getSetClear.tap(); await expect(element(by.id('clear_button'))).toExist(); await expect(element(by.id('increaseByTen_button'))).toExist(); await expect(element(by.id('storedNumber_text'))).toExist(); }); + it('should store value in async storage', async () => { const storedNumberText = await element(by.id('storedNumber_text')); const increaseByTenButton = await element(by.id('increaseByTen_button')); @@ -65,6 +70,10 @@ describe('Async Storage', () => { }); describe('merge item test', () => { + beforeAll(async () => { + await device.openURL({ url: 'rnc-asyncstorage://unset-delegate' }); + }); + it('should be visible', async () => { await test_mergeItem.tap(); await expect(element(by.id('saveItem_button'))).toExist(); @@ -139,4 +148,72 @@ describe('Async Storage', () => { expect(storyText).toHaveText(newStory); }); }); + + describe('get / set / clear item delegate test', () => { + beforeAll(async () => { + await device.openURL({ url: 'rnc-asyncstorage://set-delegate' }); + }); + + it('should be visible', async () => { + await test_getSetClear.tap(); + await expect(element(by.id('clear_button'))).toExist(); + await expect(element(by.id('increaseByTen_button'))).toExist(); + await expect(element(by.id('storedNumber_text'))).toExist(); + }); + + it('should store value in async storage', async () => { + const storedNumberText = await element(by.id('storedNumber_text')); + const increaseByTenButton = await element(by.id('increaseByTen_button')); + + await expect(storedNumberText).toHaveText(''); + + const tapTimes = Math.round(Math.random() * 9) + 1; + + for (let i = 0; i < tapTimes; i++) { + await increaseByTenButton.tap(); + } + + await expect(storedNumberText).toHaveText(`${tapTimes * 10}`); + await restartButton.tap(); + await expect(storedNumberText).toHaveText(`${tapTimes * 10}`); + }); + + it('should clear item', async () => { + const storedNumberText = await element(by.id('storedNumber_text')); + const increaseByTenButton = await element(by.id('increaseByTen_button')); + const clearButton = await element(by.id('clear_button')); + + await increaseByTenButton.tap(); + await clearButton.tap(); + await restartButton.tap(); + await expect(storedNumberText).toHaveText(''); + }); + }); + + describe('merge item delegate test', () => { + beforeAll(async () => { + await device.openURL({ url: 'rnc-asyncstorage://set-delegate' }); + }); + + it('should be visible', async () => { + await test_mergeItem.tap(); + await expect(element(by.id('saveItem_button'))).toExist(); + await expect(element(by.id('mergeItem_button'))).toExist(); + await expect(element(by.id('restoreItem_button'))).toExist(); + await expect(element(by.id('testInput-name'))).toExist(); + await expect(element(by.id('testInput-age'))).toExist(); + await expect(element(by.id('testInput-eyes'))).toExist(); + await expect(element(by.id('testInput-shoe'))).toExist(); + }); + + it('should crash when merging items in async storage', async () => { + const buttonMergeItem = await element(by.id('mergeItem_button')); + try { + await buttonMergeItem.tap(); + fail(); + } catch { + // Expected + } + }); + }); }); diff --git a/example/ios/AsyncStorageExample/AppDelegate.m b/example/ios/AsyncStorageExample/AppDelegate.m index b3f825d3..e355f470 100644 --- a/example/ios/AsyncStorageExample/AppDelegate.m +++ b/example/ios/AsyncStorageExample/AppDelegate.m @@ -14,6 +14,7 @@ @implementation AppDelegate { NSMutableDictionary *_memoryStorage; + __weak RCTBridge *_bridge; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions @@ -66,9 +67,29 @@ - (void)didLoadJavaScript:(NSNotification *)note return; } + _bridge = bridge; [self addDevMenuItemsForBridge:bridge]; } +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + if (![url.scheme isEqualToString:@"rnc-asyncstorage"]) { + return NO; + } + + if ([url.host isEqualToString:@"set-delegate"]) { + RNCAsyncStorage *asyncStorage = [_bridge moduleForClass:[RNCAsyncStorage class]]; + asyncStorage.delegate = self; + } else if ([url.host isEqualToString:@"unset-delegate"]) { + RNCAsyncStorage *asyncStorage = [_bridge moduleForClass:[RNCAsyncStorage class]]; + asyncStorage.delegate = nil; + } + + return YES; +} + #pragma mark - RNCAsyncStorageDelegate - (void)allKeys:(nonnull RNCAsyncStorageResultCallback)completion @@ -80,6 +101,8 @@ - (void)mergeValues:(nonnull NSArray *)values forKeys:(nonnull NSArray *)keys completion:(nonnull RNCAsyncStorageResultCallback)block { + [NSException raise:@"Unimplemented" + format:@"%@ is unimplemented", NSStringFromSelector(_cmd)]; } - (void)removeAllValues:(nonnull RNCAsyncStorageCompletion)completion diff --git a/example/ios/AsyncStorageExample/Info.plist b/example/ios/AsyncStorageExample/Info.plist index 1c36c0fe..7ac62b4f 100644 --- a/example/ios/AsyncStorageExample/Info.plist +++ b/example/ios/AsyncStorageExample/Info.plist @@ -20,10 +20,36 @@ 1.0 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + org.reactjs.native.example.AsyncStorageExample + CFBundleURLSchemes + + rnc-asyncstorage + + + CFBundleVersion 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + NSLocationWhenInUseUsageDescription UILaunchStoryboardName @@ -40,21 +66,5 @@ UIViewControllerBasedStatusBarAppearance - NSLocationWhenInUseUsageDescription - - NSAppTransportSecurity - - - NSAllowsArbitraryLoads - - NSExceptionDomains - - localhost - - NSExceptionAllowsInsecureHTTPLoads - - - - From b711c63cb2836656ab93e897e7d68aa5d3b8b19c Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 27 Mar 2019 11:01:28 +0100 Subject: [PATCH 05/16] Fixed lint errors. --- example/e2e/asyncstorage.e2e.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index 59b304bb..e04e81e6 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -30,7 +30,7 @@ describe('Async Storage', () => { describe('get / set / clear item test', () => { beforeAll(async () => { - await device.openURL({ url: 'rnc-asyncstorage://unset-delegate' }); + await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); }); it('should be visible', async () => { @@ -71,7 +71,7 @@ describe('Async Storage', () => { describe('merge item test', () => { beforeAll(async () => { - await device.openURL({ url: 'rnc-asyncstorage://unset-delegate' }); + await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); }); it('should be visible', async () => { @@ -151,7 +151,7 @@ describe('Async Storage', () => { describe('get / set / clear item delegate test', () => { beforeAll(async () => { - await device.openURL({ url: 'rnc-asyncstorage://set-delegate' }); + await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); }); it('should be visible', async () => { @@ -192,7 +192,7 @@ describe('Async Storage', () => { describe('merge item delegate test', () => { beforeAll(async () => { - await device.openURL({ url: 'rnc-asyncstorage://set-delegate' }); + await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); }); it('should be visible', async () => { @@ -210,6 +210,9 @@ describe('Async Storage', () => { const buttonMergeItem = await element(by.id('mergeItem_button')); try { await buttonMergeItem.tap(); + + // Not quite sure why ESLint thinks Jest hasn't defined fail(). + // eslint-disable-next-line no-undef fail(); } catch { // Expected From eb3b262498cfda4bcbd87119db9d308a886c6208 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 27 Mar 2019 14:04:31 +0100 Subject: [PATCH 06/16] Fixed tests failing on Android. --- example/e2e/asyncstorage.e2e.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index e04e81e6..86f2ea14 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -30,7 +30,9 @@ describe('Async Storage', () => { describe('get / set / clear item test', () => { beforeAll(async () => { - await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); + if (device.getPlatform() === 'ios') { + await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); + } }); it('should be visible', async () => { @@ -71,7 +73,9 @@ describe('Async Storage', () => { describe('merge item test', () => { beforeAll(async () => { - await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); + if (device.getPlatform() === 'ios') { + await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); + } }); it('should be visible', async () => { @@ -151,7 +155,9 @@ describe('Async Storage', () => { describe('get / set / clear item delegate test', () => { beforeAll(async () => { - await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); + if (device.getPlatform() === 'ios') { + await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); + } }); it('should be visible', async () => { @@ -192,7 +198,9 @@ describe('Async Storage', () => { describe('merge item delegate test', () => { beforeAll(async () => { - await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); + if (device.getPlatform() === 'ios') { + await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); + } }); it('should be visible', async () => { @@ -207,6 +215,11 @@ describe('Async Storage', () => { }); it('should crash when merging items in async storage', async () => { + if (device.getPlatform() === 'android') { + // Not yet supported. + return; + } + const buttonMergeItem = await element(by.id('mergeItem_button')); try { await buttonMergeItem.tap(); From 15b722b8b92669154608741b0ec1d851319b26d9 Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy Date: Wed, 3 Apr 2019 12:40:23 +0200 Subject: [PATCH 07/16] Update README.md Co-Authored-By: tido64 <4123478+tido64@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd6693e2..f764ab9b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ getData = async () => { ``` -See docs for [api and more examples](docs/API.md), and [some advanced usage](docs/AdvancedUsage.md). +See docs for [api and more examples](docs/API.md), and [brownfield integration guide](docs/AdvancedUsage.md). ## Writing tests From ee0efe2489bd83a23a31624c8410ca4ae103ed67 Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy Date: Wed, 3 Apr 2019 12:40:30 +0200 Subject: [PATCH 08/16] Update docs/AdvancedUsage.md Co-Authored-By: tido64 <4123478+tido64@users.noreply.github.com> --- docs/AdvancedUsage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/AdvancedUsage.md b/docs/AdvancedUsage.md index f16c4f68..0202b9c6 100644 --- a/docs/AdvancedUsage.md +++ b/docs/AdvancedUsage.md @@ -1,6 +1,6 @@ # Advanced Usage -## Integrating with Existing Storage Solutions in Hybrid Apps +## Integrating with Existing Storage Solutions in RN Brownfield Apps ### iOS From 8f0a2c0365fb71537e5ede48de715bf9a6073ab6 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 3 Apr 2019 12:39:56 +0200 Subject: [PATCH 09/16] Split RNCAsyncStorageDelegate impl from AppDelegate --- .../project.pbxproj | 6 ++ .../AppDelegate+RNCAsyncStorageDelegate.h | 13 ++++ .../AppDelegate+RNCAsyncStorageDelegate.m | 65 +++++++++++++++++++ example/ios/AsyncStorageExample/AppDelegate.h | 5 +- example/ios/AsyncStorageExample/AppDelegate.m | 56 +--------------- 5 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h create mode 100644 example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m diff --git a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj index c90aa9b5..a24e19a1 100644 --- a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj +++ b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */; }; 3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DC5398C220F2C940035D3A3 /* libRNCAsyncStorage.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; }; @@ -320,6 +321,8 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = AsyncStorageExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = AsyncStorageExample/main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "AppDelegate+RNCAsyncStorageDelegate.m"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m"; sourceTree = ""; }; + 196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AppDelegate+RNCAsyncStorageDelegate.h"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h"; sourceTree = ""; }; 2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCAsyncStorage.xcodeproj; path = ../../ios/RNCAsyncStorage.xcodeproj; sourceTree = ""; }; 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; @@ -423,6 +426,8 @@ 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */, + 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, @@ -957,6 +962,7 @@ files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, + 196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h b/example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h new file mode 100644 index 00000000..5d50d691 --- /dev/null +++ b/example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "AppDelegate.h" + +#import + +@interface AppDelegate (RNCAsyncStorageDelegate) +@end diff --git a/example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m b/example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m new file mode 100644 index 00000000..3e3ded31 --- /dev/null +++ b/example/ios/AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "AppDelegate+RNCAsyncStorageDelegate.h" + +#import + +@implementation AppDelegate (RNCAsyncStorageDelegate) + +- (void)allKeys:(nonnull RNCAsyncStorageResultCallback)completion +{ + completion(self.memoryStorage.allKeys); +} + +- (void)mergeValues:(nonnull NSArray *)values + forKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)block +{ + [NSException raise:@"Unimplemented" + format:@"%@ is unimplemented", NSStringFromSelector(_cmd)]; +} + +- (void)removeAllValues:(nonnull RNCAsyncStorageCompletion)completion +{ + [self.memoryStorage removeAllObjects]; + completion(nil); +} + +- (void)removeValuesForKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + for (NSString *key in keys) { + [self.memoryStorage removeObjectForKey:key]; + } + completion(@[]); +} + +- (void)setValues:(nonnull NSArray *)values + forKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + for (NSUInteger i = 0; i < values.count; ++i) { + NSString *value = values[i]; + NSString *key = keys[i]; + [self.memoryStorage setObject:value forKey:key]; + } + completion(@[]); +} + +- (void)valuesForKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count]; + for (NSString *key in keys) { + NSString *value = self.memoryStorage[key]; + [values addObject:value == nil ? [NSNull null] : value]; + } + completion(values); +} + +@end diff --git a/example/ios/AsyncStorageExample/AppDelegate.h b/example/ios/AsyncStorageExample/AppDelegate.h index 1d413e36..fa44dcbd 100644 --- a/example/ios/AsyncStorageExample/AppDelegate.h +++ b/example/ios/AsyncStorageExample/AppDelegate.h @@ -7,10 +7,9 @@ #import -#import - -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder @property (nonatomic, strong) UIWindow *window; +@property (nonatomic, strong) NSMutableDictionary *memoryStorage; @end diff --git a/example/ios/AsyncStorageExample/AppDelegate.m b/example/ios/AsyncStorageExample/AppDelegate.m index e355f470..82183ece 100644 --- a/example/ios/AsyncStorageExample/AppDelegate.m +++ b/example/ios/AsyncStorageExample/AppDelegate.m @@ -7,13 +7,14 @@ #import "AppDelegate.h" +#import "AppDelegate+RNCAsyncStorageDelegate.h" + #import #import #import #import @implementation AppDelegate { - NSMutableDictionary *_memoryStorage; __weak RCTBridge *_bridge; } @@ -90,57 +91,4 @@ - (BOOL)application:(UIApplication *)app return YES; } -#pragma mark - RNCAsyncStorageDelegate - -- (void)allKeys:(nonnull RNCAsyncStorageResultCallback)completion -{ - completion(_memoryStorage.allKeys); -} - -- (void)mergeValues:(nonnull NSArray *)values - forKeys:(nonnull NSArray *)keys - completion:(nonnull RNCAsyncStorageResultCallback)block -{ - [NSException raise:@"Unimplemented" - format:@"%@ is unimplemented", NSStringFromSelector(_cmd)]; -} - -- (void)removeAllValues:(nonnull RNCAsyncStorageCompletion)completion -{ - [_memoryStorage removeAllObjects]; - completion(nil); -} - -- (void)removeValuesForKeys:(nonnull NSArray *)keys - completion:(nonnull RNCAsyncStorageResultCallback)completion -{ - for (NSString *key in keys) { - [_memoryStorage removeObjectForKey:key]; - } - completion(@[]); -} - -- (void)setValues:(nonnull NSArray *)values - forKeys:(nonnull NSArray *)keys - completion:(nonnull RNCAsyncStorageResultCallback)completion -{ - for (NSUInteger i = 0; i < values.count; ++i) { - NSString *value = values[i]; - NSString *key = keys[i]; - [_memoryStorage setObject:value forKey:key]; - } - completion(@[]); -} - -- (void)valuesForKeys:(nonnull NSArray *)keys - completion:(nonnull RNCAsyncStorageResultCallback)completion -{ - NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count]; - for (NSString *key in keys) { - NSString *value = _memoryStorage[key]; - [values addObject:value == nil ? [NSNull null] : value]; - } - completion(values); -} - @end From c8b8ac68c41c0fc7903adde436660eb8cafb0bb1 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 3 Apr 2019 12:42:42 +0200 Subject: [PATCH 10/16] Added horizontal rules between each method. --- docs/AdvancedUsage.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/AdvancedUsage.md b/docs/AdvancedUsage.md index 0202b9c6..bb6aa99c 100644 --- a/docs/AdvancedUsage.md +++ b/docs/AdvancedUsage.md @@ -21,7 +21,7 @@ The delegate must conform to the protocol `RNCAsyncStorageDelegate`: Returns all keys currently stored. If none, an empty array is returned. Called by `getAllKeys` in JS. -
+--- ```objc - (void)mergeValues:(NSArray *)values @@ -32,7 +32,7 @@ Called by `getAllKeys` in JS. Merges values with the corresponding values stored at specified keys. Called by `mergeItem` and `multiMerge` in JS. -
+--- ```objc - (void)removeAllValues:(RNCAsyncStorageCompletion)block; @@ -40,7 +40,7 @@ Called by `mergeItem` and `multiMerge` in JS. Removes all values from the store. Called by `clear` in JS. -
+--- ```objc - (void)removeValuesForKeys:(NSArray *)keys @@ -50,7 +50,7 @@ Removes all values from the store. Called by `clear` in JS. Removes all values associated with specified keys. Called by `removeItem` and `multiRemove` in JS. -
+--- ```objc - (void)setValues:(NSArray *)values @@ -60,7 +60,7 @@ Called by `removeItem` and `multiRemove` in JS. Sets specified key-value pairs. Called by `setItem` and `multiSet` in JS. -
+--- ```objc - (void)valuesForKeys:(NSArray *)keys @@ -70,7 +70,7 @@ Sets specified key-value pairs. Called by `setItem` and `multiSet` in JS. Returns values associated with specified keys. Called by `getItem` and `multiGet` in JS. -
+--- ```objc @optional From 5c894f75a5d983dd466468b0d32b880c3a1655c1 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 3 Apr 2019 12:44:18 +0200 Subject: [PATCH 11/16] Removed redundant tests. --- example/e2e/asyncstorage.e2e.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index 86f2ea14..966bd669 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -160,13 +160,6 @@ describe('Async Storage', () => { } }); - it('should be visible', async () => { - await test_getSetClear.tap(); - await expect(element(by.id('clear_button'))).toExist(); - await expect(element(by.id('increaseByTen_button'))).toExist(); - await expect(element(by.id('storedNumber_text'))).toExist(); - }); - it('should store value in async storage', async () => { const storedNumberText = await element(by.id('storedNumber_text')); const increaseByTenButton = await element(by.id('increaseByTen_button')); @@ -203,17 +196,6 @@ describe('Async Storage', () => { } }); - it('should be visible', async () => { - await test_mergeItem.tap(); - await expect(element(by.id('saveItem_button'))).toExist(); - await expect(element(by.id('mergeItem_button'))).toExist(); - await expect(element(by.id('restoreItem_button'))).toExist(); - await expect(element(by.id('testInput-name'))).toExist(); - await expect(element(by.id('testInput-age'))).toExist(); - await expect(element(by.id('testInput-eyes'))).toExist(); - await expect(element(by.id('testInput-shoe'))).toExist(); - }); - it('should crash when merging items in async storage', async () => { if (device.getPlatform() === 'android') { // Not yet supported. From d6ac32708c430a8a04faf14f4aaa2b37e5058d97 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 3 Apr 2019 23:16:32 +0200 Subject: [PATCH 12/16] Fixed tests failing due to inter-test deps --- example/e2e/asyncstorage.e2e.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index 966bd669..0162eb85 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -158,6 +158,7 @@ describe('Async Storage', () => { if (device.getPlatform() === 'ios') { await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); } + await test_getSetClear.tap(); }); it('should store value in async storage', async () => { @@ -194,6 +195,7 @@ describe('Async Storage', () => { if (device.getPlatform() === 'ios') { await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); } + await test_mergeItem.tap(); }); it('should crash when merging items in async storage', async () => { From 0b71efb12e8595e35de542d388f9bd6f2f6fe081 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Wed, 3 Apr 2019 23:34:38 +0200 Subject: [PATCH 13/16] Ensure all tests can be run independently --- example/e2e/asyncstorage.e2e.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index 0162eb85..0a8980cd 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -33,10 +33,10 @@ describe('Async Storage', () => { if (device.getPlatform() === 'ios') { await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); } + await test_getSetClear.tap(); }); it('should be visible', async () => { - await test_getSetClear.tap(); await expect(element(by.id('clear_button'))).toExist(); await expect(element(by.id('increaseByTen_button'))).toExist(); await expect(element(by.id('storedNumber_text'))).toExist(); @@ -76,10 +76,10 @@ describe('Async Storage', () => { if (device.getPlatform() === 'ios') { await device.openURL({url: 'rnc-asyncstorage://unset-delegate'}); } + await test_mergeItem.tap(); }); it('should be visible', async () => { - await test_mergeItem.tap(); await expect(element(by.id('saveItem_button'))).toExist(); await expect(element(by.id('mergeItem_button'))).toExist(); await expect(element(by.id('restoreItem_button'))).toExist(); From 0ebbe13b70d4722c592e75903806284eeb66a038 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Thu, 4 Apr 2019 14:44:25 +0200 Subject: [PATCH 14/16] Rewrite tests to ensure calls reach the delegate --- example/e2e/asyncstorage.e2e.js | 89 +++++++++++--- .../project.pbxproj | 6 + example/ios/AsyncStorageExample/AppDelegate.m | 7 +- .../RNCTestAsyncStorageDelegate.h | 14 +++ .../RNCTestAsyncStorageDelegate.m | 112 ++++++++++++++++++ 5 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.h create mode 100644 example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.m diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index 0a8980cd..1280c3b7 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -155,13 +155,21 @@ describe('Async Storage', () => { describe('get / set / clear item delegate test', () => { beforeAll(async () => { - if (device.getPlatform() === 'ios') { - await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); - } await test_getSetClear.tap(); + if (device.getPlatform() === 'android') { + // Not yet supported. + return; + } + + await device.openURL({url: 'rnc-asyncstorage://set-delegate'}); }); - it('should store value in async storage', async () => { + it('should store value with delegate', async () => { + if (device.getPlatform() === 'android') { + // Not yet supported. + return; + } + const storedNumberText = await element(by.id('storedNumber_text')); const increaseByTenButton = await element(by.id('increaseByTen_button')); @@ -175,10 +183,17 @@ describe('Async Storage', () => { await expect(storedNumberText).toHaveText(`${tapTimes * 10}`); await restartButton.tap(); - await expect(storedNumberText).toHaveText(`${tapTimes * 10}`); + + // The delegate will distinguish itself by always returning the stored value + 1000000 + await expect(storedNumberText).toHaveText(`${tapTimes * 10 + 1000000}`); }); - it('should clear item', async () => { + it('should clear item with delegate', async () => { + if (device.getPlatform() === 'android') { + // Not yet supported. + return; + } + const storedNumberText = await element(by.id('storedNumber_text')); const increaseByTenButton = await element(by.id('increaseByTen_button')); const clearButton = await element(by.id('clear_button')); @@ -186,7 +201,10 @@ describe('Async Storage', () => { await increaseByTenButton.tap(); await clearButton.tap(); await restartButton.tap(); - await expect(storedNumberText).toHaveText(''); + + // The delegate will distinguish itself by actually setting storing 1000000 + // instead of clearing. It will also always return the stored value + 1000000. + await expect(storedNumberText).toHaveText('2000000'); }); }); @@ -198,22 +216,61 @@ describe('Async Storage', () => { await test_mergeItem.tap(); }); - it('should crash when merging items in async storage', async () => { + it('should merge items with delegate', async () => { if (device.getPlatform() === 'android') { // Not yet supported. return; } const buttonMergeItem = await element(by.id('mergeItem_button')); - try { - await buttonMergeItem.tap(); - - // Not quite sure why ESLint thinks Jest hasn't defined fail(). - // eslint-disable-next-line no-undef - fail(); - } catch { - // Expected + const buttonRestoreItem = await element(by.id('restoreItem_button')); + + const nameInput = await element(by.id('testInput-name')); + const ageInput = await element(by.id('testInput-age')); + const eyesInput = await element(by.id('testInput-eyes')); + const shoeInput = await element(by.id('testInput-shoe')); + const storyText = await element(by.id('storyTextView')); + + const isAndroid = device.getPlatform() === 'android'; + + async function performInput() { + const name = Math.random() > 0.5 ? 'Jerry' : 'Sarah'; + const age = Math.random() > 0.5 ? '21' : '23'; + const eyesColor = Math.random() > 0.5 ? 'blue' : 'green'; + const shoeSize = Math.random() > 0.5 ? '9' : '10'; + + if (!isAndroid) { + await eyesInput.tap(); + } + await nameInput.typeText(name); + await closeKeyboard.tap(); + + if (!isAndroid) { + await eyesInput.tap(); + } + await ageInput.typeText(age); + await closeKeyboard.tap(); + + if (!isAndroid) { + await eyesInput.tap(); + } + await eyesInput.typeText(eyesColor); + await closeKeyboard.tap(); + + if (!isAndroid) { + await eyesInput.tap(); + } + await shoeInput.typeText(shoeSize); + await closeKeyboard.tap(); + + return `${name} from delegate is ${age} from delegate, has ${eyesColor} eyes and shoe size of ${shoeSize}.`; } + + const story = await performInput(); + await buttonMergeItem.tap(); + await restartButton.tap(); + await buttonRestoreItem.tap(); + expect(storyText).toHaveText(story); }); }); }); diff --git a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj index a24e19a1..16b0e1b5 100644 --- a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj +++ b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */; }; + 19C469542256303E00CA1332 /* RNCTestAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */; }; 3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DC5398C220F2C940035D3A3 /* libRNCAsyncStorage.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; }; @@ -323,6 +324,8 @@ 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "AppDelegate+RNCAsyncStorageDelegate.m"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m"; sourceTree = ""; }; 196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AppDelegate+RNCAsyncStorageDelegate.h"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h"; sourceTree = ""; }; + 19C4692D22562FD400CA1332 /* RNCTestAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNCTestAsyncStorageDelegate.h; path = AsyncStorageExample/RNCTestAsyncStorageDelegate.h; sourceTree = ""; }; + 19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNCTestAsyncStorageDelegate.m; path = AsyncStorageExample/RNCTestAsyncStorageDelegate.m; sourceTree = ""; }; 2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCAsyncStorage.xcodeproj; path = ../../ios/RNCAsyncStorage.xcodeproj; sourceTree = ""; }; 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; @@ -428,6 +431,8 @@ 13B07FB01A68108700A75B9A /* AppDelegate.m */, 196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */, 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */, + 19C4692D22562FD400CA1332 /* RNCTestAsyncStorageDelegate.h */, + 19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, @@ -961,6 +966,7 @@ buildActionMask = 2147483647; files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 19C469542256303E00CA1332 /* RNCTestAsyncStorageDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, 196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */, ); diff --git a/example/ios/AsyncStorageExample/AppDelegate.m b/example/ios/AsyncStorageExample/AppDelegate.m index 82183ece..0be1f588 100644 --- a/example/ios/AsyncStorageExample/AppDelegate.m +++ b/example/ios/AsyncStorageExample/AppDelegate.m @@ -8,6 +8,7 @@ #import "AppDelegate.h" #import "AppDelegate+RNCAsyncStorageDelegate.h" +#import "RNCTestAsyncStorageDelegate.h" #import #import @@ -16,6 +17,7 @@ @implementation AppDelegate { __weak RCTBridge *_bridge; + RNCTestAsyncStorageDelegate *_testDelegate; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions @@ -81,8 +83,11 @@ - (BOOL)application:(UIApplication *)app } if ([url.host isEqualToString:@"set-delegate"]) { + if (_testDelegate == nil) { + _testDelegate = [RNCTestAsyncStorageDelegate new]; + } RNCAsyncStorage *asyncStorage = [_bridge moduleForClass:[RNCAsyncStorage class]]; - asyncStorage.delegate = self; + asyncStorage.delegate = _testDelegate; } else if ([url.host isEqualToString:@"unset-delegate"]) { RNCAsyncStorage *asyncStorage = [_bridge moduleForClass:[RNCAsyncStorage class]]; asyncStorage.delegate = nil; diff --git a/example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.h b/example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.h new file mode 100644 index 00000000..610e49d3 --- /dev/null +++ b/example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/*! + * Implementation of |RNCAsyncStorageDelegate| used for E2E testing purposes only. + */ +@interface RNCTestAsyncStorageDelegate : NSObject +@end diff --git a/example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.m b/example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.m new file mode 100644 index 00000000..14bb8b56 --- /dev/null +++ b/example/ios/AsyncStorageExample/RNCTestAsyncStorageDelegate.m @@ -0,0 +1,112 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNCTestAsyncStorageDelegate.h" + +const NSInteger RNCTestAsyncStorageExtraValue = 1000000; + +NSString *RNCTestAddExtraValue(NSString *value) +{ + NSInteger i = [value integerValue]; + if (i == 0) { + return value; + } + return [NSString stringWithFormat:@"%ld", (long)i + RNCTestAsyncStorageExtraValue]; +} + +NSString *RNCTestRemoveExtraValue(NSString *value) +{ + NSInteger i = [value integerValue]; + if (i > RNCTestAsyncStorageExtraValue) { + i -= RNCTestAsyncStorageExtraValue; + } + return [NSString stringWithFormat:@"%ld", (long)i]; +} + +@implementation RNCTestAsyncStorageDelegate { + NSMutableDictionary *_memoryStorage; +} + +- (instancetype)init +{ + if (self = [super init]) { + _memoryStorage = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)allKeys:(RNCAsyncStorageResultCallback)completion +{ + completion(_memoryStorage.allKeys); +} + +- (void)mergeValues:(nonnull NSArray *)values + forKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + NSError *error = nil; + NSDictionary *dictionary = + [NSJSONSerialization JSONObjectWithData:[values[0] dataUsingEncoding:NSUTF8StringEncoding] + options:NSJSONReadingMutableContainers + error:&error]; + NSMutableDictionary *modified = [NSMutableDictionary dictionaryWithCapacity:dictionary.count]; + for (NSString *key in dictionary) { + NSObject *value = dictionary[key]; + if ([value isKindOfClass:[NSString class]]) { + modified[key] = [(NSString *)value stringByAppendingString:@" from delegate"]; + } else { + modified[key] = value; + } + } + + NSData *data = [NSJSONSerialization dataWithJSONObject:modified options:0 error:&error]; + NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + _memoryStorage[keys[0]] = str; + completion(@[str]); +} + +- (void)removeAllValues:(nonnull RNCAsyncStorageCompletion)completion +{ + [_memoryStorage removeAllObjects]; + completion(nil); +} + +- (void)removeValuesForKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + for (NSString *key in keys) { + [_memoryStorage + setObject:[NSString stringWithFormat:@"%ld", (long)RNCTestAsyncStorageExtraValue] + forKey:key]; + } + completion(@[]); +} + +- (void)setValues:(nonnull NSArray *)values + forKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + for (NSUInteger i = 0; i < values.count; ++i) { + NSString *value = values[i]; + NSString *key = keys[i]; + [_memoryStorage setObject:RNCTestRemoveExtraValue(value) forKey:key]; + } + completion(@[]); +} + +- (void)valuesForKeys:(nonnull NSArray *)keys + completion:(nonnull RNCAsyncStorageResultCallback)completion +{ + NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count]; + for (NSString *key in keys) { + NSString *value = _memoryStorage[key]; + [values addObject:value == nil ? [NSNull null] : RNCTestAddExtraValue(value)]; + } + completion(values); +} + +@end From 3197ae1d4eff4e63268b0c58837196485e668ccb Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Mon, 8 Apr 2019 09:55:13 +0200 Subject: [PATCH 15/16] Docs changes by krizzu :) --- docs/AdvancedUsage.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/AdvancedUsage.md b/docs/AdvancedUsage.md index bb6aa99c..c85594c6 100644 --- a/docs/AdvancedUsage.md +++ b/docs/AdvancedUsage.md @@ -1,10 +1,11 @@ -# Advanced Usage +# Integrating Async Storage with embedded React Native apps -## Integrating with Existing Storage Solutions in RN Brownfield Apps +If you're embedding React Native into native application, you can also integrate +Async Storage module, so that both worlds will use one storage solution. -### iOS +## iOS -On iOS, AsyncStorage can be controlled by the hosting app via the delegate on +AsyncStorage can be controlled by the hosting app via the delegate on `RNCAsyncStorage`: ```objc @@ -12,7 +13,11 @@ RNCAsyncStorage *asyncStorage = [bridge moduleForClass:[RNCAsyncStorage class]]; asyncStorage.delegate = self; ``` -The delegate must conform to the protocol `RNCAsyncStorageDelegate`: +### The procotol + +The delegate must conform to the `RNCAsyncStorageDelegate` protocol + +--- ```objc - (void)allKeys:(RNCAsyncStorageResultCallback)block; From e82c530eba36a54a5e64b1961ff5591c0a6419a2 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen Date: Mon, 8 Apr 2019 20:38:40 +0200 Subject: [PATCH 16/16] Empty commit to trigger a new build.