Skip to content

Commit a30fc51

Browse files
authored
feat: Allow brownfield iOS apps to handle storage. (#35)
Adds a delegate property to `RNCAsyncStorage` to allow custom handling of storage. Implements #33.
1 parent 55acf4c commit a30fc51

15 files changed

+779
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ getData = async () => {
5959

6060
```
6161

62-
See docs for [api and more examples.](docs/API.md)
62+
See docs for [api and more examples](docs/API.md), and [brownfield integration guide](docs/AdvancedUsage.md).
6363

6464
## Writing tests
6565

docs/AdvancedUsage.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Integrating Async Storage with embedded React Native apps
2+
3+
If you're embedding React Native into native application, you can also integrate
4+
Async Storage module, so that both worlds will use one storage solution.
5+
6+
## iOS
7+
8+
AsyncStorage can be controlled by the hosting app via the delegate on
9+
`RNCAsyncStorage`:
10+
11+
```objc
12+
RNCAsyncStorage *asyncStorage = [bridge moduleForClass:[RNCAsyncStorage class]];
13+
asyncStorage.delegate = self;
14+
```
15+
16+
### The procotol
17+
18+
The delegate must conform to the `RNCAsyncStorageDelegate` protocol
19+
20+
---
21+
22+
```objc
23+
- (void)allKeys:(RNCAsyncStorageResultCallback)block;
24+
```
25+
26+
Returns all keys currently stored. If none, an empty array is returned.
27+
Called by `getAllKeys` in JS.
28+
29+
---
30+
31+
```objc
32+
- (void)mergeValues:(NSArray<NSString *> *)values
33+
forKeys:(NSArray<NSString *> *)keys
34+
completion:(RNCAsyncStorageResultCallback)block;
35+
```
36+
37+
Merges values with the corresponding values stored at specified keys.
38+
Called by `mergeItem` and `multiMerge` in JS.
39+
40+
---
41+
42+
```objc
43+
- (void)removeAllValues:(RNCAsyncStorageCompletion)block;
44+
```
45+
46+
Removes all values from the store. Called by `clear` in JS.
47+
48+
---
49+
50+
```objc
51+
- (void)removeValuesForKeys:(NSArray<NSString *> *)keys
52+
completion:(RNCAsyncStorageResultCallback)block;
53+
```
54+
55+
Removes all values associated with specified keys.
56+
Called by `removeItem` and `multiRemove` in JS.
57+
58+
---
59+
60+
```objc
61+
- (void)setValues:(NSArray<NSString *> *)values
62+
forKeys:(NSArray<NSString *> *)keys
63+
completion:(RNCAsyncStorageResultCallback)block;
64+
```
65+
66+
Sets specified key-value pairs. Called by `setItem` and `multiSet` in JS.
67+
68+
---
69+
70+
```objc
71+
- (void)valuesForKeys:(NSArray<NSString *> *)keys
72+
completion:(RNCAsyncStorageResultCallback)block;
73+
```
74+
75+
Returns values associated with specified keys.
76+
Called by `getItem` and `multiGet` in JS.
77+
78+
---
79+
80+
```objc
81+
@optional
82+
@property (nonatomic, readonly, getter=isPassthrough) BOOL passthrough;
83+
```
84+
85+
**Optional:** Returns whether the delegate should be treated as a passthrough.
86+
This is useful for monitoring storage usage among other things. Returns `NO` by
87+
default.

example/e2e/asyncstorage.e2e.js

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@ describe('Async Storage', () => {
2929
});
3030

3131
describe('get / set / clear item test', () => {
32-
it('should be visible', async () => {
32+
beforeAll(async () => {
33+
if (device.getPlatform() === 'ios') {
34+
await device.openURL({url: 'rnc-asyncstorage://unset-delegate'});
35+
}
3336
await test_getSetClear.tap();
37+
});
38+
39+
it('should be visible', async () => {
3440
await expect(element(by.id('clear_button'))).toExist();
3541
await expect(element(by.id('increaseByTen_button'))).toExist();
3642
await expect(element(by.id('storedNumber_text'))).toExist();
3743
});
44+
3845
it('should store value in async storage', async () => {
3946
const storedNumberText = await element(by.id('storedNumber_text'));
4047
const increaseByTenButton = await element(by.id('increaseByTen_button'));
@@ -65,8 +72,14 @@ describe('Async Storage', () => {
6572
});
6673

6774
describe('merge item test', () => {
68-
it('should be visible', async () => {
75+
beforeAll(async () => {
76+
if (device.getPlatform() === 'ios') {
77+
await device.openURL({url: 'rnc-asyncstorage://unset-delegate'});
78+
}
6979
await test_mergeItem.tap();
80+
});
81+
82+
it('should be visible', async () => {
7083
await expect(element(by.id('saveItem_button'))).toExist();
7184
await expect(element(by.id('mergeItem_button'))).toExist();
7285
await expect(element(by.id('restoreItem_button'))).toExist();
@@ -139,4 +152,125 @@ describe('Async Storage', () => {
139152
expect(storyText).toHaveText(newStory);
140153
});
141154
});
155+
156+
describe('get / set / clear item delegate test', () => {
157+
beforeAll(async () => {
158+
await test_getSetClear.tap();
159+
if (device.getPlatform() === 'android') {
160+
// Not yet supported.
161+
return;
162+
}
163+
164+
await device.openURL({url: 'rnc-asyncstorage://set-delegate'});
165+
});
166+
167+
it('should store value with delegate', async () => {
168+
if (device.getPlatform() === 'android') {
169+
// Not yet supported.
170+
return;
171+
}
172+
173+
const storedNumberText = await element(by.id('storedNumber_text'));
174+
const increaseByTenButton = await element(by.id('increaseByTen_button'));
175+
176+
await expect(storedNumberText).toHaveText('');
177+
178+
const tapTimes = Math.round(Math.random() * 9) + 1;
179+
180+
for (let i = 0; i < tapTimes; i++) {
181+
await increaseByTenButton.tap();
182+
}
183+
184+
await expect(storedNumberText).toHaveText(`${tapTimes * 10}`);
185+
await restartButton.tap();
186+
187+
// The delegate will distinguish itself by always returning the stored value + 1000000
188+
await expect(storedNumberText).toHaveText(`${tapTimes * 10 + 1000000}`);
189+
});
190+
191+
it('should clear item with delegate', async () => {
192+
if (device.getPlatform() === 'android') {
193+
// Not yet supported.
194+
return;
195+
}
196+
197+
const storedNumberText = await element(by.id('storedNumber_text'));
198+
const increaseByTenButton = await element(by.id('increaseByTen_button'));
199+
const clearButton = await element(by.id('clear_button'));
200+
201+
await increaseByTenButton.tap();
202+
await clearButton.tap();
203+
await restartButton.tap();
204+
205+
// The delegate will distinguish itself by actually setting storing 1000000
206+
// instead of clearing. It will also always return the stored value + 1000000.
207+
await expect(storedNumberText).toHaveText('2000000');
208+
});
209+
});
210+
211+
describe('merge item delegate test', () => {
212+
beforeAll(async () => {
213+
if (device.getPlatform() === 'ios') {
214+
await device.openURL({url: 'rnc-asyncstorage://set-delegate'});
215+
}
216+
await test_mergeItem.tap();
217+
});
218+
219+
it('should merge items with delegate', async () => {
220+
if (device.getPlatform() === 'android') {
221+
// Not yet supported.
222+
return;
223+
}
224+
225+
const buttonMergeItem = await element(by.id('mergeItem_button'));
226+
const buttonRestoreItem = await element(by.id('restoreItem_button'));
227+
228+
const nameInput = await element(by.id('testInput-name'));
229+
const ageInput = await element(by.id('testInput-age'));
230+
const eyesInput = await element(by.id('testInput-eyes'));
231+
const shoeInput = await element(by.id('testInput-shoe'));
232+
const storyText = await element(by.id('storyTextView'));
233+
234+
const isAndroid = device.getPlatform() === 'android';
235+
236+
async function performInput() {
237+
const name = Math.random() > 0.5 ? 'Jerry' : 'Sarah';
238+
const age = Math.random() > 0.5 ? '21' : '23';
239+
const eyesColor = Math.random() > 0.5 ? 'blue' : 'green';
240+
const shoeSize = Math.random() > 0.5 ? '9' : '10';
241+
242+
if (!isAndroid) {
243+
await eyesInput.tap();
244+
}
245+
await nameInput.typeText(name);
246+
await closeKeyboard.tap();
247+
248+
if (!isAndroid) {
249+
await eyesInput.tap();
250+
}
251+
await ageInput.typeText(age);
252+
await closeKeyboard.tap();
253+
254+
if (!isAndroid) {
255+
await eyesInput.tap();
256+
}
257+
await eyesInput.typeText(eyesColor);
258+
await closeKeyboard.tap();
259+
260+
if (!isAndroid) {
261+
await eyesInput.tap();
262+
}
263+
await shoeInput.typeText(shoeSize);
264+
await closeKeyboard.tap();
265+
266+
return `${name} from delegate is ${age} from delegate, has ${eyesColor} eyes and shoe size of ${shoeSize}.`;
267+
}
268+
269+
const story = await performInput();
270+
await buttonMergeItem.tap();
271+
await restartButton.tap();
272+
await buttonRestoreItem.tap();
273+
expect(storyText).toHaveText(story);
274+
});
275+
});
142276
});

example/ios/AsyncStorageExample.xcodeproj/project.pbxproj

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2222
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
2323
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
24+
196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */; };
25+
19C469542256303E00CA1332 /* RNCTestAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */; };
2426
3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DC5398C220F2C940035D3A3 /* libRNCAsyncStorage.a */; };
2527
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
2628
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; };
@@ -84,6 +86,13 @@
8486
remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192;
8587
remoteInfo = React;
8688
};
89+
1990B95122398FC4009E5EA1 /* PBXContainerItemProxy */ = {
90+
isa = PBXContainerItemProxy;
91+
containerPortal = 3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */;
92+
proxyType = 1;
93+
remoteGlobalIDString = 58B511DA1A9E6C8500147676;
94+
remoteInfo = RNCAsyncStorage;
95+
};
8796
2D16E6711FA4F8DC00B85C8A /* PBXContainerItemProxy */ = {
8897
isa = PBXContainerItemProxy;
8998
containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */;
@@ -313,6 +322,10 @@
313322
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = AsyncStorageExample/Info.plist; sourceTree = "<group>"; };
314323
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = AsyncStorageExample/main.m; sourceTree = "<group>"; };
315324
146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; };
325+
196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "AppDelegate+RNCAsyncStorageDelegate.m"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m"; sourceTree = "<group>"; };
326+
196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AppDelegate+RNCAsyncStorageDelegate.h"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h"; sourceTree = "<group>"; };
327+
19C4692D22562FD400CA1332 /* RNCTestAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNCTestAsyncStorageDelegate.h; path = AsyncStorageExample/RNCTestAsyncStorageDelegate.h; sourceTree = "<group>"; };
328+
19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNCTestAsyncStorageDelegate.m; path = AsyncStorageExample/RNCTestAsyncStorageDelegate.m; sourceTree = "<group>"; };
316329
2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; };
317330
3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCAsyncStorage.xcodeproj; path = ../../ios/RNCAsyncStorage.xcodeproj; sourceTree = "<group>"; };
318331
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
@@ -329,11 +342,9 @@
329342
buildActionMask = 2147483647;
330343
files = (
331344
3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */,
332-
ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */,
333-
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */,
334-
11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */,
335-
146834051AC3E58100842450 /* libReact.a in Frameworks */,
336345
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */,
346+
11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */,
347+
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */,
337348
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */,
338349
00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */,
339350
133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */,
@@ -342,6 +353,8 @@
342353
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
343354
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
344355
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
356+
146834051AC3E58100842450 /* libReact.a in Frameworks */,
357+
ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */,
345358
);
346359
runOnlyForDeploymentPostprocessing = 0;
347360
};
@@ -416,6 +429,10 @@
416429
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
417430
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
418431
13B07FB01A68108700A75B9A /* AppDelegate.m */,
432+
196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */,
433+
196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */,
434+
19C4692D22562FD400CA1332 /* RNCTestAsyncStorageDelegate.h */,
435+
19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */,
419436
13B07FB51A68108700A75B9A /* Images.xcassets */,
420437
13B07FB61A68108700A75B9A /* Info.plist */,
421438
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
@@ -557,6 +574,7 @@
557574
buildRules = (
558575
);
559576
dependencies = (
577+
1990B95222398FC4009E5EA1 /* PBXTargetDependency */,
560578
);
561579
name = AsyncStorageExample;
562580
productName = "Hello World";
@@ -938,7 +956,7 @@
938956
);
939957
runOnlyForDeploymentPostprocessing = 0;
940958
shellPath = /bin/sh;
941-
shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh";
959+
shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh\n";
942960
};
943961
/* End PBXShellScriptBuildPhase section */
944962

@@ -948,12 +966,22 @@
948966
buildActionMask = 2147483647;
949967
files = (
950968
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
969+
19C469542256303E00CA1332 /* RNCTestAsyncStorageDelegate.m in Sources */,
951970
13B07FC11A68108700A75B9A /* main.m in Sources */,
971+
196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */,
952972
);
953973
runOnlyForDeploymentPostprocessing = 0;
954974
};
955975
/* End PBXSourcesBuildPhase section */
956976

977+
/* Begin PBXTargetDependency section */
978+
1990B95222398FC4009E5EA1 /* PBXTargetDependency */ = {
979+
isa = PBXTargetDependency;
980+
name = RNCAsyncStorage;
981+
targetProxy = 1990B95122398FC4009E5EA1 /* PBXContainerItemProxy */;
982+
};
983+
/* End PBXTargetDependency section */
984+
957985
/* Begin PBXVariantGroup section */
958986
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
959987
isa = PBXVariantGroup;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "AppDelegate.h"
9+
10+
#import <RNCAsyncStorage/RNCAsyncStorageDelegate.h>
11+
12+
@interface AppDelegate (RNCAsyncStorageDelegate) <RNCAsyncStorageDelegate>
13+
@end

0 commit comments

Comments
 (0)