ソースを参照

screen lifecycle

Daniel Zlotin 7 年 前
コミット
293fb6adcd

+ 4
- 0
ios/RNN.m ファイルの表示

15
 
15
 
16
 @implementation RNN
16
 @implementation RNN
17
 
17
 
18
+# pragma mark public
19
+
18
 +(instancetype)instance
20
 +(instancetype)instance
19
 {
21
 {
20
 	static RNN *sharedInstance = nil;
22
 	static RNN *sharedInstance = nil;
43
 	self.bridge = [[RCTBridge alloc] initWithBundleURL:jsCodeLocation moduleProvider:nil launchOptions:launchOptions];
45
 	self.bridge = [[RCTBridge alloc] initWithBundleURL:jsCodeLocation moduleProvider:nil launchOptions:launchOptions];
44
 }
46
 }
45
 
47
 
48
+# pragma mark private
49
+
46
 -(void)registerForJsEvents
50
 -(void)registerForJsEvents
47
 {
51
 {
48
 	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onJavaScriptLoaded) name:RCTJavaScriptDidLoadNotification object:self.bridge];
52
 	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onJavaScriptLoaded) name:RCTJavaScriptDidLoadNotification object:self.bridge];

+ 3
- 3
ios/RNNBridgeModule.m ファイルの表示

15
 RCT_EXPORT_METHOD(setRoot:(NSDictionary*)layout)
15
 RCT_EXPORT_METHOD(setRoot:(NSDictionary*)layout)
16
 {
16
 {
17
 	[self assertReady];
17
 	[self assertReady];
18
-	UIApplication.sharedApplication.delegate.window.rootViewController = [[RNNControllerFactory new] createRootViewController:layout];
18
+	UIApplication.sharedApplication.delegate.window.rootViewController = [[RNNControllerFactory new] createLayout:layout];
19
 	[UIApplication.sharedApplication.delegate.window makeKeyAndVisible];
19
 	[UIApplication.sharedApplication.delegate.window makeKeyAndVisible];
20
 }
20
 }
21
 
21
 
22
 RCT_EXPORT_METHOD(push:(NSString*)containerId layout:(NSDictionary*)layout)
22
 RCT_EXPORT_METHOD(push:(NSString*)containerId layout:(NSDictionary*)layout)
23
 {
23
 {
24
 	[self assertReady];
24
 	[self assertReady];
25
-	//TODO make this not shitty
26
-	UIViewController* newVc = [[RNNControllerFactory new] createRootViewController:layout];
25
+	//TODO implement correctly
26
+	UIViewController* newVc = [[RNNControllerFactory new] createLayout:layout];
27
 	id vc = [UIApplication.sharedApplication.delegate.window.rootViewController childViewControllers][0];
27
 	id vc = [UIApplication.sharedApplication.delegate.window.rootViewController childViewControllers][0];
28
 	[[vc navigationController]pushViewController:newVc animated:true];
28
 	[[vc navigationController]pushViewController:newVc animated:true];
29
 }
29
 }

+ 1
- 1
ios/RNNControllerFactory.h ファイルの表示

4
 
4
 
5
 @interface RNNControllerFactory : NSObject
5
 @interface RNNControllerFactory : NSObject
6
 
6
 
7
--(UIViewController*)createRootViewController:(NSDictionary*)layout;
7
+-(UIViewController*)createLayout:(NSDictionary*)layout;
8
 
8
 
9
 @end
9
 @end

+ 5
- 1
ios/RNNControllerFactory.m ファイルの表示

5
 
5
 
6
 @implementation RNNControllerFactory
6
 @implementation RNNControllerFactory
7
 
7
 
8
--(UIViewController *)createRootViewController:(NSDictionary *)layout
8
+# pragma mark public
9
+
10
+-(UIViewController *)createLayout:(NSDictionary *)layout
9
 {
11
 {
10
 	return [self fromTree:layout];
12
 	return [self fromTree:layout];
11
 }
13
 }
12
 
14
 
15
+# pragma mark private
16
+
13
 -(UIViewController*)fromTree:(NSDictionary*)json
17
 -(UIViewController*)fromTree:(NSDictionary*)json
14
 {
18
 {
15
 	RNNLayoutNode* node = [RNNLayoutNode create:json];
19
 	RNNLayoutNode* node = [RNNLayoutNode create:json];

+ 7
- 7
ios/RNNEventEmitter.m ファイルの表示

10
 static NSString* const containerStart	= @"RNN.containerStart";
10
 static NSString* const containerStart	= @"RNN.containerStart";
11
 static NSString* const containerStop	= @"RNN.containerStop";
11
 static NSString* const containerStop	= @"RNN.containerStop";
12
 
12
 
13
+-(NSArray<NSString *> *)supportedEvents
14
+{
15
+	return @[onAppLaunched, containerStart, containerStop];
16
+}
17
+
13
 # pragma mark public
18
 # pragma mark public
14
 
19
 
15
 -(void)sendOnAppLaunched
20
 -(void)sendOnAppLaunched
19
 
24
 
20
 -(void)sendContainerStart:(NSString *)containerId
25
 -(void)sendContainerStart:(NSString *)containerId
21
 {
26
 {
22
-	[self send:containerStart body:@{@"id": containerId}];
27
+	[self send:containerStart body:containerId];
23
 }
28
 }
24
 
29
 
25
 -(void)sendContainerStop:(NSString *)containerId
30
 -(void)sendContainerStop:(NSString *)containerId
26
 {
31
 {
27
-	[self send:containerStop body:@{@"id": containerId}];
32
+	[self send:containerStop body:containerId];
28
 }
33
 }
29
 
34
 
30
 # pragma mark private
35
 # pragma mark private
31
 
36
 
32
--(NSArray<NSString *> *)supportedEvents
33
-{
34
-	return @[onAppLaunched, containerStart, containerStop];
35
-}
36
-
37
 -(void)send:(NSString *)eventName body:(id)body
37
 -(void)send:(NSString *)eventName body:(id)body
38
 {
38
 {
39
 	[[RNN.instance.bridge moduleForClass:[RNNEventEmitter class]] sendEventWithName:eventName body:body];
39
 	[[RNN.instance.bridge moduleForClass:[RNNEventEmitter class]] sendEventWithName:eventName body:body];

+ 1
- 1
playground/e2e/app.test.js ファイルの表示

24
     expect(elementByLabel('This is a side menu screen')).toBeVisible();
24
     expect(elementByLabel('This is a side menu screen')).toBeVisible();
25
   });
25
   });
26
 
26
 
27
-  xit('screen lifecycle', () => {
27
+  it('screen lifecycle', () => {
28
     elementByLabel('Switch to lifecycle screen').tap();
28
     elementByLabel('Switch to lifecycle screen').tap();
29
     expect(elementByLabel('onStart!')).toBeVisible();
29
     expect(elementByLabel('onStart!')).toBeVisible();
30
   });
30
   });

+ 4
- 3
playground/src/containers/WelcomeScreen.js ファイルの表示

7
   constructor(props) {
7
   constructor(props) {
8
     super(props);
8
     super(props);
9
     this.onClickPush = this.onClickPush.bind(this);
9
     this.onClickPush = this.onClickPush.bind(this);
10
-    this.onClickLifecycleScreen = this.onClickLifecycleScreen.bind(this);
11
   }
10
   }
12
 
11
 
13
   render() {
12
   render() {
91
   }
90
   }
92
 
91
 
93
   onClickLifecycleScreen() {
92
   onClickLifecycleScreen() {
94
-    Navigation.on(this.props.id).push({
95
-      name: 'com.example.LifecycleScreen'
93
+    Navigation.setRoot({
94
+      container: {
95
+        name: 'com.example.LifecycleScreen'
96
+      }
96
     });
97
     });
97
   }
98
   }
98
 }
99
 }

+ 4
- 0
src/adapters/NativeEventsReceiver.js ファイルの表示

9
     this.emitter.addListener('RNN.containerStart', callback);
9
     this.emitter.addListener('RNN.containerStart', callback);
10
   }
10
   }
11
 
11
 
12
+  containerStop(callback) {
13
+    this.emitter.addListener('RNN.containerStop', callback);
14
+  }
15
+
12
   appLaunched(callback) {
16
   appLaunched(callback) {
13
     this.emitter.addListener('RNN.appLaunched', callback);
17
     this.emitter.addListener('RNN.appLaunched', callback);
14
   }
18
   }

+ 9
- 1
src/adapters/NativeEventsReceiver.test.js ファイルの表示

19
     expect(eventEmitterMock.addListener).toHaveBeenCalledWith('RNN.appLaunched', callback);
19
     expect(eventEmitterMock.addListener).toHaveBeenCalledWith('RNN.appLaunched', callback);
20
   });
20
   });
21
 
21
 
22
-  it('register for internal events', () => {
22
+  it('containerStart', () => {
23
     const callback = jest.fn();
23
     const callback = jest.fn();
24
     uut.containerStart(callback);
24
     uut.containerStart(callback);
25
     expect(callback).not.toHaveBeenCalled();
25
     expect(callback).not.toHaveBeenCalled();
26
     expect(eventEmitterMock.addListener).toHaveBeenCalledTimes(1);
26
     expect(eventEmitterMock.addListener).toHaveBeenCalledTimes(1);
27
     expect(eventEmitterMock.addListener).toHaveBeenCalledWith('RNN.containerStart', callback);
27
     expect(eventEmitterMock.addListener).toHaveBeenCalledWith('RNN.containerStart', callback);
28
   });
28
   });
29
+
30
+  it('containerStop', () => {
31
+    const callback = jest.fn();
32
+    uut.containerStop(callback);
33
+    expect(callback).not.toHaveBeenCalled();
34
+    expect(eventEmitterMock.addListener).toHaveBeenCalledTimes(1);
35
+    expect(eventEmitterMock.addListener).toHaveBeenCalledWith('RNN.containerStop', callback);
36
+  });
29
 });
37
 });

+ 13
- 0
src/containers/ContainerWrapper.js ファイルの表示

31
         store.cleanId(this.state.id);
31
         store.cleanId(this.state.id);
32
       }
32
       }
33
 
33
 
34
+      onStart() {
35
+        if (this.originalContainerRef.onStart) {
36
+          this.originalContainerRef.onStart();
37
+        }
38
+      }
39
+
40
+      onStop() {
41
+        if (this.originalContainerRef.onStop) {
42
+          this.originalContainerRef.onStop();
43
+        }
44
+      }
45
+
34
       componentWillReceiveProps(nextProps) {
46
       componentWillReceiveProps(nextProps) {
35
         this.setState({
47
         this.setState({
36
           allProps: _.merge({}, nextProps, store.getPropsForContainerId(this.state.id))
48
           allProps: _.merge({}, nextProps, store.getPropsForContainerId(this.state.id))
40
       render() {
52
       render() {
41
         return (
53
         return (
42
           <OriginalContainer
54
           <OriginalContainer
55
+            ref={(r) => this.originalContainerRef = r}
43
             {...this.state.allProps}
56
             {...this.state.allProps}
44
             id={this.state.id}
57
             id={this.state.id}
45
             key={this.state.id}
58
             key={this.state.id}

+ 30
- 3
src/containers/ContainerWrapper.test.js ファイルの表示

120
     expect(store.getRefForId('container1')).toBeUndefined();
120
     expect(store.getRefForId('container1')).toBeUndefined();
121
   });
121
   });
122
 
122
 
123
-  xdescribe('container lifecycle', () => {
123
+  it('holds ref to OriginalContainer', () => {
124
+    const NavigationContainer = ContainerWrapper.wrap(containerName, MyContainer, store);
125
+    const tree = renderer.create(<NavigationContainer id={'container1'} />);
126
+    expect(tree.getInstance().originalContainerRef).toBe(myContainerRef);
127
+  });
128
+
129
+  it('cleans ref to internal container on unount', () => {
130
+    const NavigationContainer = ContainerWrapper.wrap(containerName, MyContainer, store);
131
+    const tree = renderer.create(<NavigationContainer id={'container1'} />);
132
+    const instance = tree.getInstance();
133
+    expect(instance.originalContainerRef).not.toBeFalsy();
134
+    tree.unmount();
135
+    expect(instance.originalContainerRef).toBeFalsy();
136
+  });
137
+
138
+  describe('container lifecycle', () => {
124
     const onStartCallback = jest.fn();
139
     const onStartCallback = jest.fn();
125
     const onStopCallback = jest.fn();
140
     const onStopCallback = jest.fn();
126
 
141
 
134
       }
149
       }
135
     }
150
     }
136
 
151
 
137
-    it('', () => {
138
-      //
152
+    it('calls onStart on OriginalContainer', () => {
153
+      const NavigationContainer = ContainerWrapper.wrap(containerName, MyLifecycleContainer, store);
154
+      const tree = renderer.create(<NavigationContainer id={'container1'} />);
155
+      expect(onStartCallback).toHaveBeenCalledTimes(0);
156
+      tree.getInstance().onStart();
157
+      expect(onStartCallback).toHaveBeenCalledTimes(1);
158
+    });
159
+
160
+    it('calls onSop on OriginalContainer', () => {
161
+      const NavigationContainer = ContainerWrapper.wrap(containerName, MyLifecycleContainer, store);
162
+      const tree = renderer.create(<NavigationContainer id={'container1'} />);
163
+      expect(onStopCallback).toHaveBeenCalledTimes(0);
164
+      tree.getInstance().onStop();
165
+      expect(onStopCallback).toHaveBeenCalledTimes(1);
139
     });
166
     });
140
   });
167
   });
141
 });
168
 });

+ 21
- 0
src/containers/Lifecycle.js ファイルの表示

1
+export default class Lifecycle {
2
+  constructor(store) {
3
+    this.store = store;
4
+    this.containerStart = this.containerStart.bind(this);
5
+    this.containerStop = this.containerStop.bind(this);
6
+  }
7
+
8
+  containerStart(id) {
9
+    const ref = this.store.getRefForId(id);
10
+    if (ref && ref.onStart) {
11
+      ref.onStart();
12
+    }
13
+  }
14
+
15
+  containerStop(id) {
16
+    const ref = this.store.getRefForId(id);
17
+    if (ref && ref.onStop) {
18
+      ref.onStop();
19
+    }
20
+  }
21
+}

+ 56
- 0
src/containers/Lifecycle.test.js ファイルの表示

1
+import Lifecycle from './Lifecycle';
2
+import Store from '../containers/Store';
3
+
4
+describe('Lifecycle', () => {
5
+  let uut;
6
+  let mockRef;
7
+
8
+  beforeEach(() => {
9
+    mockRef = {
10
+      onStart: jest.fn(),
11
+      onStop: jest.fn()
12
+    };
13
+    store = new Store();
14
+    store.setRefForId('myUniqueId', mockRef);
15
+
16
+    uut = new Lifecycle(store);
17
+  });
18
+
19
+  describe('containerStart', () => {
20
+    it('calls onStart on container ref from store', () => {
21
+      uut.containerStart('myUniqueId');
22
+      expect(mockRef.onStart).toHaveBeenCalledTimes(1);
23
+    });
24
+
25
+    it('skips undefined refs', () => {
26
+      uut.containerStart('myUniqueId2');
27
+      expect(mockRef.onStart).not.toHaveBeenCalled();
28
+    });
29
+
30
+    it('skips unimplemented onStart', () => {
31
+      mockRef = {};
32
+      expect(mockRef.onStart).toBeUndefined();
33
+      store.setRefForId('myUniqueId', mockRef);
34
+      uut.containerStart('myUniqueId');
35
+    });
36
+  });
37
+
38
+  describe('containerStop', () => {
39
+    it('calls onStop on container ref from store', () => {
40
+      uut.containerStop('myUniqueId');
41
+      expect(mockRef.onStop).toHaveBeenCalledTimes(1);
42
+    });
43
+
44
+    it('skips undefined refs', () => {
45
+      uut.containerStop('myUniqueId2');
46
+      expect(mockRef.onStop).not.toHaveBeenCalled();
47
+    });
48
+
49
+    it('skips unimplemented onStop', () => {
50
+      mockRef = {};
51
+      expect(mockRef.onStop).toBeUndefined();
52
+      store.setRefForId('myUniqueId', mockRef);
53
+      uut.containerStop('myUniqueId');
54
+    });
55
+  });
56
+});

+ 4
- 14
src/events/PrivateEventsListener.js ファイルの表示

1
 import _ from 'lodash';
1
 import _ from 'lodash';
2
+import Lifecycle from '../containers/Lifecycle';
2
 
3
 
3
 export default class PrivateEventsListener {
4
 export default class PrivateEventsListener {
4
   constructor(nativeEventsReceiver, store) {
5
   constructor(nativeEventsReceiver, store) {
5
     this.nativeEventsReceiver = nativeEventsReceiver;
6
     this.nativeEventsReceiver = nativeEventsReceiver;
6
-    this.store = store;
7
+    this.lifecycle = new Lifecycle(store);
7
   }
8
   }
8
 
9
 
9
   listenAndHandlePrivateEvents() {
10
   listenAndHandlePrivateEvents() {
10
-    this.nativeEventsReceiver.containerStart(this._handleContainerStart);
11
-  }
12
-
13
-  _handleContainerStart(params) {
14
-    // const id = params.id;
15
-    // try {
16
-    //   const ref = this.store.getRefForId(id);
17
-    //   if (ref && ref.onStart) {
18
-    //     ref.onStart();
19
-    //   }
20
-    // } catch (e) {
21
-    //   console.warn(e);
22
-    // }
11
+    this.nativeEventsReceiver.containerStart(this.lifecycle.containerStart);
12
+    this.nativeEventsReceiver.containerStop(this.lifecycle.containerStop);
23
   }
13
   }
24
 }
14
 }

+ 18
- 2
src/events/PrivateEventsListener.test.js ファイルの表示

1
 import _ from 'lodash';
1
 import _ from 'lodash';
2
 import PrivateEventsListener from './PrivateEventsListener';
2
 import PrivateEventsListener from './PrivateEventsListener';
3
 import NativeEventsReceiver from '../adapters/NativeEventsReceiver.mock';
3
 import NativeEventsReceiver from '../adapters/NativeEventsReceiver.mock';
4
+import Store from '../containers/Store';
4
 
5
 
5
 describe('PrivateEventsListener', () => {
6
 describe('PrivateEventsListener', () => {
6
   let uut;
7
   let uut;
7
   let nativeEventsReceiver;
8
   let nativeEventsReceiver;
9
+  let store;
8
 
10
 
9
   beforeEach(() => {
11
   beforeEach(() => {
10
     nativeEventsReceiver = new NativeEventsReceiver();
12
     nativeEventsReceiver = new NativeEventsReceiver();
11
-    uut = new PrivateEventsListener(nativeEventsReceiver);
13
+    store = new Store();
14
+    uut = new PrivateEventsListener(nativeEventsReceiver, store);
12
   });
15
   });
13
 
16
 
14
-  it('registers for private events against nativeEventsReceiver', () => {
17
+  it('register and handle containerStart', () => {
18
+    const mockRef = {
19
+      onStart: jest.fn()
20
+    };
21
+    store.setRefForId('myContainerId', mockRef);
15
     uut.listenAndHandlePrivateEvents();
22
     uut.listenAndHandlePrivateEvents();
16
     expect(nativeEventsReceiver.containerStart).toHaveBeenCalledTimes(1);
23
     expect(nativeEventsReceiver.containerStart).toHaveBeenCalledTimes(1);
17
     const callbackFunction = nativeEventsReceiver.containerStart.mock.calls[0][0];
24
     const callbackFunction = nativeEventsReceiver.containerStart.mock.calls[0][0];
18
     expect(callbackFunction).toBeInstanceOf(Function);
25
     expect(callbackFunction).toBeInstanceOf(Function);
26
+
27
+    expect(mockRef.onStart).not.toHaveBeenCalled();
19
     callbackFunction('myContainerId');
28
     callbackFunction('myContainerId');
29
+
30
+    expect(mockRef.onStart).toHaveBeenCalledTimes(1);
31
+  });
32
+
33
+  it('register and listen containerStop', () => {
34
+    uut.listenAndHandlePrivateEvents();
35
+    expect(nativeEventsReceiver.containerStop).toHaveBeenCalledTimes(1);
20
   });
36
   });
21
 });
37
 });