| Flutter iOS Embedder |

FlutterViewControllerTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import <OCMock/OCMock.h>
6 #import <XCTest/XCTest.h>
7
8 #include "flutter/fml/platform/darwin/message_loop_darwin.h"
9 #import "flutter/lib/ui/window/platform_configuration.h"
10 #include "flutter/lib/ui/window/pointer_data.h"
11 #import "flutter/lib/ui/window/viewport_metrics.h"
12 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
13 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
14 #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h"
16 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
17 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
18 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h"
19 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
20 #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h"
21 #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
22 #import "flutter/shell/platform/embedder/embedder.h"
23 #import "flutter/third_party/spring_animation/spring_animation.h"
24
25 FLUTTER_ASSERT_ARC
26
27 using namespace flutter::testing;
28
29 @interface FlutterEngine ()
30 - (FlutterTextInputPlugin*)textInputPlugin;
31 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
32 callback:(nullable FlutterKeyEventCallback)callback
33 userData:(nullable void*)userData;
34 - (fml::RefPtr<fml::TaskRunner>)uiTaskRunner;
35 @end
36
37 /// Sometimes we have to use a custom mock to avoid retain cycles in OCMock.
38 /// Used for testing low memory notification.
39 @interface FlutterEnginePartialMock : FlutterEngine
40 @property(nonatomic, strong) FlutterBasicMessageChannel* lifecycleChannel;
41 @property(nonatomic, strong) FlutterBasicMessageChannel* keyEventChannel;
42 @property(nonatomic, weak) FlutterViewController* viewController;
43 @property(nonatomic, strong) FlutterTextInputPlugin* textInputPlugin;
44 @property(nonatomic, assign) BOOL didCallNotifyLowMemory;
45 - (FlutterTextInputPlugin*)textInputPlugin;
46 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
47 callback:(nullable FlutterKeyEventCallback)callback
48 userData:(nullable void*)userData;
49 @end
50
51 @implementation FlutterEnginePartialMock
52 @synthesize viewController;
53 @synthesize lifecycleChannel;
54 @synthesize keyEventChannel;
55 @synthesize textInputPlugin;
56
57 - (void)notifyLowMemory {
58 _didCallNotifyLowMemory = YES;
59 }
60
61 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
62 callback:(FlutterKeyEventCallback)callback
63 userData:(void*)userData API_AVAILABLE(ios(9.0)) {
64 if (callback == nil) {
65 return;
66 }
67 // NSAssert(callback != nullptr, @"Invalid callback");
68 // Response is async, so we have to post it to the run loop instead of calling
69 // it directly.
70 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
71 ^() {
72 callback(true, userData);
73 });
74 }
75 @end
76
77 @interface FlutterEngine ()
78 - (BOOL)createShell:(NSString*)entrypoint
79 libraryURI:(NSString*)libraryURI
80 initialRoute:(NSString*)initialRoute;
81 - (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)packet;
82 - (void)updateViewportMetrics:(flutter::ViewportMetrics)viewportMetrics;
83 - (void)attachView;
84 @end
85
86 @interface FlutterEngine (TestLowMemory)
87 - (void)notifyLowMemory;
88 @end
89
90 extern NSNotificationName const FlutterViewControllerWillDealloc;
91
92 /// A simple mock class for FlutterEngine.
93 ///
94 /// OCMClassMock can't be used for FlutterEngine sometimes because OCMock retains arguments to
95 /// invocations and since the init for FlutterViewController calls a method on the
96 /// FlutterEngine it creates a retain cycle that stops us from testing behaviors related to
97 /// deleting FlutterViewControllers.
98 ///
99 /// Used for testing deallocation.
100 @interface MockEngine : NSObject
101 @property(nonatomic, strong) FlutterDartProject* project;
102 @end
103
104 @implementation MockEngine
105 - (FlutterViewController*)viewController {
106 return nil;
107 }
108 - (void)setViewController:(FlutterViewController*)viewController {
109 // noop
110 }
111 @end
112
113 @interface FlutterKeyboardManager (Tests)
114 @property(nonatomic, retain, readonly)
115 NSMutableArray<id<FlutterKeyPrimaryResponder>>* primaryResponders;
116 @end
117
118 @interface FlutterEmbedderKeyResponder (Tests)
119 @property(nonatomic, copy, readonly) FlutterSendKeyEvent sendEvent;
120 @end
121
122 @interface FlutterViewController (Tests)
123
124 @property(nonatomic, assign) double targetViewInsetBottom;
125 @property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;
126 @property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
127 @property(nonatomic, strong) VSyncClient* keyboardAnimationVSyncClient;
128 @property(nonatomic, strong) VSyncClient* touchRateCorrectionVSyncClient;
129
130 - (void)createTouchRateCorrectionVSyncClientIfNeeded;
131 - (void)surfaceUpdated:(BOOL)appeared;
132 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
133 - (void)handlePressEvent:(FlutterUIPressProxy*)press
134 nextAction:(void (^)())next API_AVAILABLE(ios(13.4));
135 - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer;
136 - (void)updateViewportMetricsIfNeeded;
137 - (void)onUserSettingsChanged:(NSNotification*)notification;
138 - (void)applicationWillTerminate:(NSNotification*)notification;
139 - (void)goToApplicationLifecycle:(nonnull NSString*)state;
140 - (void)handleKeyboardNotification:(NSNotification*)notification;
141 - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(int)keyboardMode;
142 - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification;
143 - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification;
144 - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame;
145 - (void)startKeyBoardAnimation:(NSTimeInterval)duration;
146 - (UIView*)keyboardAnimationView;
147 - (SpringAnimation*)keyboardSpringAnimation;
148 - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation;
149 - (void)setUpKeyboardAnimationVsyncClient:
150 (FlutterKeyboardAnimationCallback)keyboardAnimationCallback;
151 - (void)ensureViewportMetricsIsCorrect;
152 - (void)invalidateKeyboardAnimationVSyncClient;
153 - (void)addInternalPlugins;
154 - (flutter::PointerData)generatePointerDataForFake;
155 - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
156 initialRoute:(nullable NSString*)initialRoute;
157 - (void)applicationBecameActive:(NSNotification*)notification;
158 - (void)applicationWillResignActive:(NSNotification*)notification;
159 - (void)applicationWillTerminate:(NSNotification*)notification;
160 - (void)applicationDidEnterBackground:(NSNotification*)notification;
161 - (void)applicationWillEnterForeground:(NSNotification*)notification;
162 - (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0));
163 - (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0));
164 - (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0));
165 - (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0));
166 - (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0));
167 - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches;
168 @end
169
170 @interface FlutterViewControllerTest : XCTestCase
171 @property(nonatomic, strong) id mockEngine;
172 @property(nonatomic, strong) id mockTextInputPlugin;
173 @property(nonatomic, strong) id messageSent;
174 - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback;
175 @end
176
177 @interface UITouch ()
178
179 @property(nonatomic, readwrite) UITouchPhase phase;
180
181 @end
182
183 @interface VSyncClient (Testing)
184
185 - (CADisplayLink*)getDisplayLink;
186
187 @end
188
189 @implementation FlutterViewControllerTest
190
191 - (void)setUp {
192 self.mockEngine = OCMClassMock([FlutterEngine class]);
193 self.mockTextInputPlugin = OCMClassMock([FlutterTextInputPlugin class]);
194 OCMStub([self.mockEngine textInputPlugin]).andReturn(self.mockTextInputPlugin);
195 self.messageSent = nil;
196 }
197
198 - (void)tearDown {
199 // We stop mocking here to avoid retain cycles that stop
200 // FlutterViewControllers from deallocing.
201 [self.mockEngine stopMocking];
202 self.mockEngine = nil;
203 self.mockTextInputPlugin = nil;
204 self.messageSent = nil;
205 }
206
207 - (id)setUpMockScreen {
208 UIScreen* mockScreen = OCMClassMock([UIScreen class]);
209 // iPhone 14 pixels
210 CGRect screenBounds = CGRectMake(0, 0, 1170, 2532);
211 OCMStub([mockScreen bounds]).andReturn(screenBounds);
212 CGFloat screenScale = 1;
213 OCMStub([mockScreen scale]).andReturn(screenScale);
214
215 return mockScreen;
216 }
217
218 - (id)setUpMockView:(FlutterViewController*)viewControllerMock
219 screen:(UIScreen*)screen
220 viewFrame:(CGRect)viewFrame
221 convertedFrame:(CGRect)convertedFrame {
222 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
223 id mockView = OCMClassMock([UIView class]);
224 OCMStub([mockView frame]).andReturn(viewFrame);
225 OCMStub([mockView convertRect:viewFrame toCoordinateSpace:[OCMArg any]])
226 .andReturn(convertedFrame);
227 OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView);
228
229 return mockView;
230 }
231
232 - (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient {
233 FlutterEngine* engine = [[FlutterEngine alloc] init];
234 [engine runWithEntrypoint:nil];
235 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
236 nibName:nil
237 bundle:nil];
238 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
239 [viewControllerMock loadView];
240 [viewControllerMock viewDidLoad];
241 OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]);
242 }
243
244 - (void)testStartKeyboardAnimationWillInvokeSetupKeyboardSpringAnimationIfNeeded {
245 FlutterEngine* engine = [[FlutterEngine alloc] init];
246 [engine runWithEntrypoint:nil];
247 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
248 nibName:nil
249 bundle:nil];
250 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
251 viewControllerMock.targetViewInsetBottom = 100;
252 [viewControllerMock startKeyBoardAnimation:0.25];
253
254 CAAnimation* keyboardAnimation =
255 [[viewControllerMock keyboardAnimationView].layer animationForKey:@"position"];
256
257 OCMVerify([viewControllerMock setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation]);
258 }
259
260 - (void)testSetupKeyboardSpringAnimationIfNeeded {
261 FlutterEngine* engine = [[FlutterEngine alloc] init];
262 [engine runWithEntrypoint:nil];
263 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
264 nibName:nil
265 bundle:nil];
266 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
267 UIScreen* screen = [self setUpMockScreen];
268 CGRect viewFrame = screen.bounds;
269 [self setUpMockView:viewControllerMock
270 screen:screen
271 viewFrame:viewFrame
272 convertedFrame:viewFrame];
273
274 // Null check.
275 [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:nil];
276 SpringAnimation* keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation];
277 XCTAssertTrue(keyboardSpringAnimation == nil);
278
279 // CAAnimation that is not a CASpringAnimation.
280 CABasicAnimation* nonSpringAnimation = [CABasicAnimation animation];
281 nonSpringAnimation.duration = 1.0;
282 nonSpringAnimation.fromValue = [NSNumber numberWithFloat:0.0];
283 nonSpringAnimation.toValue = [NSNumber numberWithFloat:1.0];
284 nonSpringAnimation.keyPath = @"position";
285 [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:nonSpringAnimation];
286 keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation];
287
288 XCTAssertTrue(keyboardSpringAnimation == nil);
289
290 // CASpringAnimation.
291 CASpringAnimation* springAnimation = [CASpringAnimation animation];
292 springAnimation.mass = 1.0;
293 springAnimation.stiffness = 100.0;
294 springAnimation.damping = 10.0;
295 springAnimation.keyPath = @"position";
296 springAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
297 springAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
298 [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:springAnimation];
299 keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation];
300 XCTAssertTrue(keyboardSpringAnimation != nil);
301 }
302
303 - (void)testKeyboardAnimationIsShowingAndCompounding {
304 FlutterEngine* engine = [[FlutterEngine alloc] init];
305 [engine runWithEntrypoint:nil];
306 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
307 nibName:nil
308 bundle:nil];
309 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
310 UIScreen* screen = [self setUpMockScreen];
311 CGRect viewFrame = screen.bounds;
312 [self setUpMockView:viewControllerMock
313 screen:screen
314 viewFrame:viewFrame
315 convertedFrame:viewFrame];
316
317 BOOL isLocal = YES;
318 CGFloat screenHeight = screen.bounds.size.height;
319 CGFloat screenWidth = screen.bounds.size.height;
320
321 // Start show keyboard animation.
322 CGRect initialShowKeyboardBeginFrame = CGRectMake(0, screenHeight, screenWidth, 250);
323 CGRect initialShowKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500);
324 NSNotification* fakeNotification = [NSNotification
325 notificationWithName:UIKeyboardWillChangeFrameNotification
326 object:nil
327 userInfo:@{
328 @"UIKeyboardFrameBeginUserInfoKey" : @(initialShowKeyboardBeginFrame),
329 @"UIKeyboardFrameEndUserInfoKey" : @(initialShowKeyboardEndFrame),
330 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
331 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
332 }];
333 viewControllerMock.targetViewInsetBottom = 0;
334 [viewControllerMock handleKeyboardNotification:fakeNotification];
335 BOOL isShowingAnimation1 = viewControllerMock.keyboardAnimationIsShowing;
336 XCTAssertTrue(isShowingAnimation1);
337
338 // Start compounding show keyboard animation.
339 CGRect compoundingShowKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250);
340 CGRect compoundingShowKeyboardEndFrame = CGRectMake(0, screenHeight - 500, screenWidth, 500);
341 fakeNotification = [NSNotification
342 notificationWithName:UIKeyboardWillChangeFrameNotification
343 object:nil
344 userInfo:@{
345 @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingShowKeyboardBeginFrame),
346 @"UIKeyboardFrameEndUserInfoKey" : @(compoundingShowKeyboardEndFrame),
347 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
348 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
349 }];
350
351 [viewControllerMock handleKeyboardNotification:fakeNotification];
352 BOOL isShowingAnimation2 = viewControllerMock.keyboardAnimationIsShowing;
353 XCTAssertTrue(isShowingAnimation2);
354 XCTAssertTrue(isShowingAnimation1 == isShowingAnimation2);
355
356 // Start hide keyboard animation.
357 CGRect initialHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 500, screenWidth, 250);
358 CGRect initialHideKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500);
359 fakeNotification = [NSNotification
360 notificationWithName:UIKeyboardWillChangeFrameNotification
361 object:nil
362 userInfo:@{
363 @"UIKeyboardFrameBeginUserInfoKey" : @(initialHideKeyboardBeginFrame),
364 @"UIKeyboardFrameEndUserInfoKey" : @(initialHideKeyboardEndFrame),
365 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
366 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
367 }];
368
369 [viewControllerMock handleKeyboardNotification:fakeNotification];
370 BOOL isShowingAnimation3 = viewControllerMock.keyboardAnimationIsShowing;
371 XCTAssertFalse(isShowingAnimation3);
372 XCTAssertTrue(isShowingAnimation2 != isShowingAnimation3);
373
374 // Start compounding hide keyboard animation.
375 CGRect compoundingHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250);
376 CGRect compoundingHideKeyboardEndFrame = CGRectMake(0, screenHeight, screenWidth, 500);
377 fakeNotification = [NSNotification
378 notificationWithName:UIKeyboardWillChangeFrameNotification
379 object:nil
380 userInfo:@{
381 @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingHideKeyboardBeginFrame),
382 @"UIKeyboardFrameEndUserInfoKey" : @(compoundingHideKeyboardEndFrame),
383 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
384 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
385 }];
386
387 [viewControllerMock handleKeyboardNotification:fakeNotification];
388 BOOL isShowingAnimation4 = viewControllerMock.keyboardAnimationIsShowing;
389 XCTAssertFalse(isShowingAnimation4);
390 XCTAssertTrue(isShowingAnimation3 == isShowingAnimation4);
391 }
392
393 - (void)testShouldIgnoreKeyboardNotification {
394 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
395 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
396 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
397 nibName:nil
398 bundle:nil];
399 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
400 UIScreen* screen = [self setUpMockScreen];
401 CGRect viewFrame = screen.bounds;
402 [self setUpMockView:viewControllerMock
403 screen:screen
404 viewFrame:viewFrame
405 convertedFrame:viewFrame];
406
407 CGFloat screenWidth = screen.bounds.size.width;
408 CGFloat screenHeight = screen.bounds.size.height;
409 CGRect emptyKeyboard = CGRectZero;
410 CGRect zeroHeightKeyboard = CGRectMake(0, 0, screenWidth, 0);
411 CGRect validKeyboardEndFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
412 BOOL isLocal = NO;
413
414 // Hide notification, valid keyboard
415 NSNotification* notification =
416 [NSNotification notificationWithName:UIKeyboardWillHideNotification
417 object:nil
418 userInfo:@{
419 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
420 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
421 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
422 }];
423
424 BOOL shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
425 XCTAssertTrue(shouldIgnore == NO);
426
427 // All zero keyboard
428 isLocal = YES;
429 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
430 object:nil
431 userInfo:@{
432 @"UIKeyboardFrameEndUserInfoKey" : @(emptyKeyboard),
433 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
434 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
435 }];
436 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
437 XCTAssertTrue(shouldIgnore == YES);
438
439 // Zero height keyboard
440 isLocal = NO;
441 notification =
442 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
443 object:nil
444 userInfo:@{
445 @"UIKeyboardFrameEndUserInfoKey" : @(zeroHeightKeyboard),
446 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
447 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
448 }];
449 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
450 XCTAssertTrue(shouldIgnore == NO);
451
452 // Valid keyboard, triggered from another app
453 isLocal = NO;
454 notification =
455 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
456 object:nil
457 userInfo:@{
458 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
459 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
460 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
461 }];
462 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
463 XCTAssertTrue(shouldIgnore == YES);
464
465 // Valid keyboard
466 isLocal = YES;
467 notification =
468 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
469 object:nil
470 userInfo:@{
471 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
472 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
473 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
474 }];
475 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
476 XCTAssertTrue(shouldIgnore == NO);
477
478 if (@available(iOS 13.0, *)) {
479 // noop
480 } else {
481 // Valid keyboard, keyboard is in background
482 OCMStub([viewControllerMock isKeyboardInOrTransitioningFromBackground]).andReturn(YES);
483
484 isLocal = YES;
485 notification =
486 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
487 object:nil
488 userInfo:@{
489 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
490 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
491 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
492 }];
493 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
494 XCTAssertTrue(shouldIgnore == YES);
495 }
496 }
497 - (void)testKeyboardAnimationWillNotCrashWhenEngineDestroyed {
498 FlutterEngine* engine = [[FlutterEngine alloc] init];
499 [engine runWithEntrypoint:nil];
500 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
501 nibName:nil
502 bundle:nil];
503 [viewController setUpKeyboardAnimationVsyncClient:^(fml::TimePoint){
504 }];
505 [engine destroyContext];
506 }
507
508 - (void)testKeyboardAnimationWillWaitUIThreadVsync {
509 // We need to make sure the new viewport metrics get sent after the
510 // begin frame event has processed. And this test is to expect that the callback
511 // will sync with UI thread. So just simulate a lot of works on UI thread and
512 // test the keyboard animation callback will execute until UI task completed.
513 // Related issue: https://github.com/flutter/flutter/issues/120555.
514
515 FlutterEngine* engine = [[FlutterEngine alloc] init];
516 [engine runWithEntrypoint:nil];
517 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
518 nibName:nil
519 bundle:nil];
520 // Post a task to UI thread to block the thread.
521 const int delayTime = 1;
522 [engine uiTaskRunner]->PostTask([] { sleep(delayTime); });
523 XCTestExpectation* expectation = [self expectationWithDescription:@"keyboard animation callback"];
524
525 __block CFTimeInterval fulfillTime;
526 FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
527 fulfillTime = CACurrentMediaTime();
528 [expectation fulfill];
529 };
530 CFTimeInterval startTime = CACurrentMediaTime();
531 [viewController setUpKeyboardAnimationVsyncClient:callback];
532 [self waitForExpectationsWithTimeout:5.0 handler:nil];
533 XCTAssertTrue(fulfillTime - startTime > delayTime);
534 }
535
536 - (void)testCalculateKeyboardAttachMode {
537 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
538 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
539 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
540 nibName:nil
541 bundle:nil];
542
543 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
544 UIScreen* screen = [self setUpMockScreen];
545 CGRect viewFrame = screen.bounds;
546 [self setUpMockView:viewControllerMock
547 screen:screen
548 viewFrame:viewFrame
549 convertedFrame:viewFrame];
550
551 CGFloat screenWidth = screen.bounds.size.width;
552 CGFloat screenHeight = screen.bounds.size.height;
553
554 // hide notification
555 CGRect keyboardFrame = CGRectZero;
556 NSNotification* notification =
557 [NSNotification notificationWithName:UIKeyboardWillHideNotification
558 object:nil
559 userInfo:@{
560 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
561 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
562 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
563 }];
564 FlutterKeyboardMode keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
565 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
566
567 // all zeros
568 keyboardFrame = CGRectZero;
569 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
570 object:nil
571 userInfo:@{
572 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
573 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
574 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
575 }];
576 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
577 XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating);
578
579 // 0 height
580 keyboardFrame = CGRectMake(0, 0, screenWidth, 0);
581 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
582 object:nil
583 userInfo:@{
584 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
585 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
586 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
587 }];
588 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
589 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
590
591 // floating
592 keyboardFrame = CGRectMake(0, 0, 320, 320);
593 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
594 object:nil
595 userInfo:@{
596 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
597 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
598 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
599 }];
600 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
601 XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating);
602
603 // undocked
604 keyboardFrame = CGRectMake(0, 0, screenWidth, 320);
605 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
606 object:nil
607 userInfo:@{
608 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
609 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
610 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
611 }];
612 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
613 XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating);
614
615 // docked
616 keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
617 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
618 object:nil
619 userInfo:@{
620 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
621 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
622 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
623 }];
624 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
625 XCTAssertTrue(keyboardMode == FlutterKeyboardModeDocked);
626
627 // docked - rounded values
628 CGFloat longDecimalHeight = 320.666666666666666;
629 keyboardFrame = CGRectMake(0, screenHeight - longDecimalHeight, screenWidth, longDecimalHeight);
630 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
631 object:nil
632 userInfo:@{
633 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
634 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
635 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
636 }];
637 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
638 XCTAssertTrue(keyboardMode == FlutterKeyboardModeDocked);
639
640 // hidden - rounded values
641 keyboardFrame = CGRectMake(0, screenHeight - .0000001, screenWidth, longDecimalHeight);
642 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
643 object:nil
644 userInfo:@{
645 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
646 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
647 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
648 }];
649 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
650 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
651
652 // hidden
653 keyboardFrame = CGRectMake(0, screenHeight, screenWidth, 320);
654 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
655 object:nil
656 userInfo:@{
657 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
658 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
659 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
660 }];
661 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
662 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
663 }
664
665 - (void)testCalculateMultitaskingAdjustment {
666 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
667 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
668 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
669 nibName:nil
670 bundle:nil];
671 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
672
673 UIScreen* screen = [self setUpMockScreen];
674 CGFloat screenWidth = screen.bounds.size.width;
675 CGFloat screenHeight = screen.bounds.size.height;
676 CGRect screenRect = screen.bounds;
677 CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40);
678 CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40);
679 CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300);
680 id mockView = [self setUpMockView:viewControllerMock
681 screen:screen
682 viewFrame:viewOrigFrame
683 convertedFrame:convertedViewFrame];
684 id mockTraitCollection = OCMClassMock([UITraitCollection class]);
685 OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad);
686 OCMStub([mockTraitCollection horizontalSizeClass]).andReturn(UIUserInterfaceSizeClassCompact);
687 OCMStub([mockTraitCollection verticalSizeClass]).andReturn(UIUserInterfaceSizeClassRegular);
688 OCMStub([mockView traitCollection]).andReturn(mockTraitCollection);
689
690 CGFloat adjustment = [viewControllerMock calculateMultitaskingAdjustment:screenRect
691 keyboardFrame:keyboardFrame];
692 XCTAssertTrue(adjustment == 20);
693 }
694
695 - (void)testCalculateKeyboardInset {
696 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
697 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
698 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
699 nibName:nil
700 bundle:nil];
701 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
702 UIScreen* screen = [self setUpMockScreen];
703 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
704
705 CGFloat screenWidth = screen.bounds.size.width;
706 CGFloat screenHeight = screen.bounds.size.height;
707 CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40);
708 CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40);
709 CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300);
710
711 [self setUpMockView:viewControllerMock
712 screen:screen
713 viewFrame:viewOrigFrame
714 convertedFrame:convertedViewFrame];
715
716 CGFloat inset = [viewControllerMock calculateKeyboardInset:keyboardFrame
717 keyboardMode:FlutterKeyboardModeDocked];
718 XCTAssertTrue(inset == 300 * screen.scale);
719 }
720
721 - (void)testHandleKeyboardNotification {
722 FlutterEngine* engine = [[FlutterEngine alloc] init];
723 [engine runWithEntrypoint:nil];
724 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
725 nibName:nil
726 bundle:nil];
727 // keyboard is empty
728 UIScreen* screen = [self setUpMockScreen];
729 CGFloat screenWidth = screen.bounds.size.width;
730 CGFloat screenHeight = screen.bounds.size.height;
731 CGRect keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
732 CGRect viewFrame = screen.bounds;
733 BOOL isLocal = YES;
734 NSNotification* notification =
735 [NSNotification notificationWithName:UIKeyboardWillShowNotification
736 object:nil
737 userInfo:@{
738 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
739 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
740 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
741 }];
742 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
743 [self setUpMockView:viewControllerMock
744 screen:screen
745 viewFrame:viewFrame
746 convertedFrame:viewFrame];
747 viewControllerMock.targetViewInsetBottom = 0;
748 XCTestExpectation* expectation = [self expectationWithDescription:@"update viewport"];
749 OCMStub([viewControllerMock updateViewportMetricsIfNeeded]).andDo(^(NSInvocation* invocation) {
750 [expectation fulfill];
751 });
752
753 [viewControllerMock handleKeyboardNotification:notification];
754 XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 320 * screen.scale);
755 OCMVerify([viewControllerMock startKeyBoardAnimation:0.25]);
756 [self waitForExpectationsWithTimeout:5.0 handler:nil];
757 }
758
759 - (void)testEnsureBottomInsetIsZeroWhenKeyboardDismissed {
760 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
761 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
762 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
763 nibName:nil
764 bundle:nil];
765
766 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
767 CGRect keyboardFrame = CGRectZero;
768 BOOL isLocal = YES;
769 NSNotification* fakeNotification =
770 [NSNotification notificationWithName:UIKeyboardWillHideNotification
771 object:nil
772 userInfo:@{
773 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
774 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
775 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
776 }];
777
778 viewControllerMock.targetViewInsetBottom = 10;
779 [viewControllerMock handleKeyboardNotification:fakeNotification];
780 XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 0);
781 }
782
783 - (void)testEnsureViewportMetricsWillInvokeAndDisplayLinkWillInvalidateInViewDidDisappear {
784 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
785 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
786 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
787 nibName:nil
788 bundle:nil];
789 id viewControllerMock = OCMPartialMock(viewController);
790 [viewControllerMock viewDidDisappear:YES];
791 OCMVerify([viewControllerMock ensureViewportMetricsIsCorrect]);
792 OCMVerify([viewControllerMock invalidateKeyboardAnimationVSyncClient]);
793 }
794
795 - (void)testViewDidDisappearDoesntPauseEngineWhenNotTheViewController {
796 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
797 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
798 mockEngine.lifecycleChannel = lifecycleChannel;
799 FlutterViewController* viewControllerA =
800 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
801 FlutterViewController* viewControllerB =
802 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
803 id viewControllerMock = OCMPartialMock(viewControllerA);
804 OCMStub([viewControllerMock surfaceUpdated:NO]);
805 mockEngine.viewController = viewControllerB;
806 [viewControllerA viewDidDisappear:NO];
807 OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]);
808 OCMReject([viewControllerMock surfaceUpdated:[OCMArg any]]);
809 }
810
811 - (void)testAppWillTerminateViewDidDestroyTheEngine {
812 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
813 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
814 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
815 nibName:nil
816 bundle:nil];
817 id viewControllerMock = OCMPartialMock(viewController);
818 OCMStub([viewControllerMock goToApplicationLifecycle:@"AppLifecycleState.detached"]);
819 OCMStub([mockEngine destroyContext]);
820 [viewController applicationWillTerminate:nil];
821 OCMVerify([viewControllerMock goToApplicationLifecycle:@"AppLifecycleState.detached"]);
822 OCMVerify([mockEngine destroyContext]);
823 }
824
825 - (void)testViewDidDisappearDoesPauseEngineWhenIsTheViewController {
826 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
827 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
828 mockEngine.lifecycleChannel = lifecycleChannel;
829 __weak FlutterViewController* weakViewController;
830 @autoreleasepool {
831 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
832 nibName:nil
833 bundle:nil];
834 weakViewController = viewController;
835 id viewControllerMock = OCMPartialMock(viewController);
836 OCMStub([viewControllerMock surfaceUpdated:NO]);
837 [viewController viewDidDisappear:NO];
838 OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]);
839 OCMVerify([viewControllerMock surfaceUpdated:NO]);
840 }
841 XCTAssertNil(weakViewController);
842 }
843
844 - (void)
845 testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillAppear {
846 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
847 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
848 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
849 nibName:nil
850 bundle:nil];
851 [viewController viewWillAppear:YES];
852 OCMVerify([viewController onUserSettingsChanged:nil]);
853 }
854
855 - (void)
856 testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillAppear {
857 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
858 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
859 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
860 nibName:nil
861 bundle:nil];
862 mockEngine.viewController = nil;
863 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
864 nibName:nil
865 bundle:nil];
866 mockEngine.viewController = nil;
867 mockEngine.viewController = viewControllerB;
868 [viewControllerA viewWillAppear:YES];
869 OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]);
870 }
871
872 - (void)
873 testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewDidAppear {
874 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
875 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
876 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
877 nibName:nil
878 bundle:nil];
879 [viewController viewDidAppear:YES];
880 OCMVerify([viewController onUserSettingsChanged:nil]);
881 }
882
883 - (void)
884 testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewDidAppear {
885 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
886 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
887 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
888 nibName:nil
889 bundle:nil];
890 mockEngine.viewController = nil;
891 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
892 nibName:nil
893 bundle:nil];
894 mockEngine.viewController = nil;
895 mockEngine.viewController = viewControllerB;
896 [viewControllerA viewDidAppear:YES];
897 OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]);
898 }
899
900 - (void)
901 testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillDisappear {
902 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
903 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
904 mockEngine.lifecycleChannel = lifecycleChannel;
905 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
906 nibName:nil
907 bundle:nil];
908 mockEngine.viewController = viewController;
909 [viewController viewWillDisappear:NO];
910 OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]);
911 }
912
913 - (void)
914 testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillDisappear {
915 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
916 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
917 mockEngine.lifecycleChannel = lifecycleChannel;
918 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
919 nibName:nil
920 bundle:nil];
921 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
922 nibName:nil
923 bundle:nil];
924 mockEngine.viewController = viewControllerB;
925 [viewControllerA viewDidDisappear:NO];
926 OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]);
927 }
928
929 - (void)testUpdateViewportMetricsIfNeeded_DoesntInvokeEngineWhenNotTheViewController {
930 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
931 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
932 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
933 nibName:nil
934 bundle:nil];
935 mockEngine.viewController = nil;
936 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
937 nibName:nil
938 bundle:nil];
939 mockEngine.viewController = viewControllerB;
940 [viewControllerA updateViewportMetricsIfNeeded];
941 flutter::ViewportMetrics viewportMetrics;
942 OCMVerify(never(), [mockEngine updateViewportMetrics:viewportMetrics]);
943 }
944
945 - (void)testUpdateViewportMetricsIfNeeded_DoesInvokeEngineWhenIsTheViewController {
946 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
947 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
948 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
949 nibName:nil
950 bundle:nil];
951 mockEngine.viewController = viewController;
952 flutter::ViewportMetrics viewportMetrics;
953 OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs();
954 [viewController updateViewportMetricsIfNeeded];
955 OCMVerifyAll(mockEngine);
956 }
957
958 - (void)testUpdateViewportMetricsIfNeeded_DoesNotInvokeEngineWhenShouldBeIgnoredDuringRotation {
959 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
960 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
961 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
962 nibName:nil
963 bundle:nil];
964 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
965 UIScreen* screen = [self setUpMockScreen];
966 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
967 mockEngine.viewController = viewController;
968
969 id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator));
970 OCMStub([mockCoordinator transitionDuration]).andReturn(0.5);
971
972 // Mimic the device rotation.
973 [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator];
974 // Should not trigger the engine call when during rotation.
975 [viewController updateViewportMetricsIfNeeded];
976
977 OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]);
978 }
979
980 - (void)testViewWillTransitionToSize_DoesDelayEngineCallIfNonZeroDuration {
981 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
982 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
983 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
984 nibName:nil
985 bundle:nil];
986 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
987 UIScreen* screen = [self setUpMockScreen];
988 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
989 mockEngine.viewController = viewController;
990
991 // Mimic the device rotation with non-zero transition duration.
992 NSTimeInterval transitionDuration = 0.5;
993 id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator));
994 OCMStub([mockCoordinator transitionDuration]).andReturn(transitionDuration);
995
996 flutter::ViewportMetrics viewportMetrics;
997 OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs();
998
999 [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator];
1000 // Should not immediately call the engine (this request should be ignored).
1001 [viewController updateViewportMetricsIfNeeded];
1002 OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]);
1003
1004 // Should delay the engine call for half of the transition duration.
1005 // Wait for additional transitionDuration to allow updateViewportMetrics calls if any.
1006 XCTWaiterResult result = [XCTWaiter
1007 waitForExpectations:@[ [self expectationWithDescription:@"Waiting for rotation duration"] ]
1008 timeout:transitionDuration];
1009 XCTAssertEqual(result, XCTWaiterResultTimedOut);
1010
1011 OCMVerifyAll(mockEngine);
1012 }
1013
1014 - (void)testViewWillTransitionToSize_DoesNotDelayEngineCallIfZeroDuration {
1015 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1016 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1017 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1018 nibName:nil
1019 bundle:nil];
1020 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
1021 UIScreen* screen = [self setUpMockScreen];
1022 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
1023 mockEngine.viewController = viewController;
1024
1025 // Mimic the device rotation with zero transition duration.
1026 id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator));
1027 OCMStub([mockCoordinator transitionDuration]).andReturn(0);
1028
1029 flutter::ViewportMetrics viewportMetrics;
1030 OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs();
1031
1032 // Should immediately trigger the engine call, without delay.
1033 [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator];
1034 [viewController updateViewportMetricsIfNeeded];
1035
1036 OCMVerifyAll(mockEngine);
1037 }
1038
1039 - (void)testViewDidLoadDoesntInvokeEngineWhenNotTheViewController {
1040 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1041 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1042 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
1043 nibName:nil
1044 bundle:nil];
1045 mockEngine.viewController = nil;
1046 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
1047 nibName:nil
1048 bundle:nil];
1049 mockEngine.viewController = viewControllerB;
1050 UIView* view = viewControllerA.view;
1051 XCTAssertNotNil(view);
1052 OCMVerify(never(), [mockEngine attachView]);
1053 }
1054
1055 - (void)testViewDidLoadDoesInvokeEngineWhenIsTheViewController {
1056 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1057 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1058 mockEngine.viewController = nil;
1059 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1060 nibName:nil
1061 bundle:nil];
1062 mockEngine.viewController = viewController;
1063 UIView* view = viewController.view;
1064 XCTAssertNotNil(view);
1065 OCMVerify(times(1), [mockEngine attachView]);
1066 }
1067
1068 - (void)testViewDidLoadDoesntInvokeEngineAttachViewWhenEngineNeedsLaunch {
1069 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1070 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1071 mockEngine.viewController = nil;
1072 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1073 nibName:nil
1074 bundle:nil];
1075 // sharedSetupWithProject sets the engine needs to be launched.
1076 [viewController sharedSetupWithProject:nil initialRoute:nil];
1077 mockEngine.viewController = viewController;
1078 UIView* view = viewController.view;
1079 XCTAssertNotNil(view);
1080 OCMVerify(never(), [mockEngine attachView]);
1081 }
1082
1083 - (void)testSplashScreenViewRemoveNotCrash {
1084 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:nil];
1085 [engine runWithEntrypoint:nil];
1086 FlutterViewController* flutterViewController =
1087 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1088 [flutterViewController setSplashScreenView:[[UIView alloc] init]];
1089 [flutterViewController setSplashScreenView:nil];
1090 }
1091
1092 - (void)testInternalPluginsWeakPtrNotCrash {
1093 FlutterSendKeyEvent sendEvent;
1094 @autoreleasepool {
1095 FlutterViewController* vc = [[FlutterViewController alloc] initWithProject:nil
1096 nibName:nil
1097 bundle:nil];
1098 [vc addInternalPlugins];
1099 FlutterKeyboardManager* keyboardManager = vc.keyboardManager;
1100 FlutterEmbedderKeyResponder* keyPrimaryResponder = (FlutterEmbedderKeyResponder*)
1101 [(NSArray<id<FlutterKeyPrimaryResponder>>*)keyboardManager.primaryResponders firstObject];
1102 sendEvent = [keyPrimaryResponder sendEvent];
1103 }
1104
1105 if (sendEvent) {
1106 sendEvent({}, nil, nil);
1107 }
1108 }
1109
1110 // Regression test for https://github.com/flutter/engine/pull/32098.
1111 - (void)testInternalPluginsInvokeInViewDidLoad {
1112 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1113 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1114 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1115 nibName:nil
1116 bundle:nil];
1117 UIView* view = viewController.view;
1118 // The implementation in viewDidLoad requires the viewControllers.viewLoaded is true.
1119 // Accessing the view to make sure the view loads in the memory,
1120 // which makes viewControllers.viewLoaded true.
1121 XCTAssertNotNil(view);
1122 [viewController viewDidLoad];
1123 OCMVerify([viewController addInternalPlugins]);
1124 }
1125
1126 - (void)testBinaryMessenger {
1127 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1128 nibName:nil
1129 bundle:nil];
1130 XCTAssertNotNil(vc);
1131 id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1132 OCMStub([self.mockEngine binaryMessenger]).andReturn(messenger);
1133 XCTAssertEqual(vc.binaryMessenger, messenger);
1134 OCMVerify([self.mockEngine binaryMessenger]);
1135 }
1136
1137 - (void)testViewControllerIsReleased {
1138 __weak FlutterViewController* weakViewController;
1139 __weak UIView* weakView;
1140 @autoreleasepool {
1141 FlutterEngine* engine = [[FlutterEngine alloc] init];
1142
1143 [engine runWithEntrypoint:nil];
1144 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1145 nibName:nil
1146 bundle:nil];
1147 weakViewController = viewController;
1148 [viewController viewDidLoad];
1149 weakView = viewController.view;
1150 XCTAssertTrue([viewController.view isKindOfClass:[FlutterView class]]);
1151 }
1152 XCTAssertNil(weakViewController);
1153 XCTAssertNil(weakView);
1154 }
1155
1156 #pragma mark - Platform Brightness
1157
1158 - (void)testItReportsLightPlatformBrightnessByDefault {
1159 // Setup test.
1160 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1161 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1162
1163 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1164 nibName:nil
1165 bundle:nil];
1166
1167 // Exercise behavior under test.
1168 [vc traitCollectionDidChange:nil];
1169
1170 // Verify behavior.
1171 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1172 return [message[@"platformBrightness"] isEqualToString:@"light"];
1173 }]]);
1174
1175 // Clean up mocks
1176 [settingsChannel stopMocking];
1177 }
1178
1179 - (void)testItReportsPlatformBrightnessWhenViewWillAppear {
1180 // Setup test.
1181 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1182 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1183 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1184 OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
1185 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1186 nibName:nil
1187 bundle:nil];
1188
1189 // Exercise behavior under test.
1190 [vc viewWillAppear:false];
1191
1192 // Verify behavior.
1193 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1194 return [message[@"platformBrightness"] isEqualToString:@"light"];
1195 }]]);
1196
1197 // Clean up mocks
1198 [settingsChannel stopMocking];
1199 }
1200
1201 - (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt {
1202 if (@available(iOS 13, *)) {
1203 // noop
1204 } else {
1205 return;
1206 }
1207
1208 // Setup test.
1209 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1210 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1211 id mockTraitCollection =
1212 [self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
1213
1214 // We partially mock the real FlutterViewController to act as the OS and report
1215 // the UITraitCollection of our choice. Mocking the object under test is not
1216 // desirable, but given that the OS does not offer a DI approach to providing
1217 // our own UITraitCollection, this seems to be the least bad option.
1218 id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine
1219 nibName:nil
1220 bundle:nil]);
1221 OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
1222
1223 // Exercise behavior under test.
1224 [partialMockVC traitCollectionDidChange:nil];
1225
1226 // Verify behavior.
1227 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1228 return [message[@"platformBrightness"] isEqualToString:@"dark"];
1229 }]]);
1230
1231 // Clean up mocks
1232 [partialMockVC stopMocking];
1233 [settingsChannel stopMocking];
1234 [mockTraitCollection stopMocking];
1235 }
1236
1237 // Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle,
1238 // which is set to the given "style".
1239 - (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style {
1240 id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1241 OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style);
1242 return mockTraitCollection;
1243 }
1244
1245 #pragma mark - Platform Contrast
1246
1247 - (void)testItReportsNormalPlatformContrastByDefault {
1248 if (@available(iOS 13, *)) {
1249 // noop
1250 } else {
1251 return;
1252 }
1253
1254 // Setup test.
1255 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1256 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1257
1258 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1259 nibName:nil
1260 bundle:nil];
1261
1262 // Exercise behavior under test.
1263 [vc traitCollectionDidChange:nil];
1264
1265 // Verify behavior.
1266 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1267 return [message[@"platformContrast"] isEqualToString:@"normal"];
1268 }]]);
1269
1270 // Clean up mocks
1271 [settingsChannel stopMocking];
1272 }
1273
1274 - (void)testItReportsPlatformContrastWhenViewWillAppear {
1275 if (@available(iOS 13, *)) {
1276 // noop
1277 } else {
1278 return;
1279 }
1280 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1281 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1282
1283 // Setup test.
1284 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1285 OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
1286 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1287 nibName:nil
1288 bundle:nil];
1289
1290 // Exercise behavior under test.
1291 [vc viewWillAppear:false];
1292
1293 // Verify behavior.
1294 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1295 return [message[@"platformContrast"] isEqualToString:@"normal"];
1296 }]]);
1297
1298 // Clean up mocks
1299 [settingsChannel stopMocking];
1300 }
1301
1302 - (void)testItReportsHighContrastWhenTraitCollectionRequestsIt {
1303 if (@available(iOS 13, *)) {
1304 // noop
1305 } else {
1306 return;
1307 }
1308
1309 // Setup test.
1310 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1311 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1312
1313 id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh];
1314
1315 // We partially mock the real FlutterViewController to act as the OS and report
1316 // the UITraitCollection of our choice. Mocking the object under test is not
1317 // desirable, but given that the OS does not offer a DI approach to providing
1318 // our own UITraitCollection, this seems to be the least bad option.
1319 id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine
1320 nibName:nil
1321 bundle:nil]);
1322 OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
1323
1324 // Exercise behavior under test.
1325 [partialMockVC traitCollectionDidChange:mockTraitCollection];
1326
1327 // Verify behavior.
1328 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1329 return [message[@"platformContrast"] isEqualToString:@"high"];
1330 }]]);
1331
1332 // Clean up mocks
1333 [partialMockVC stopMocking];
1334 [settingsChannel stopMocking];
1335 [mockTraitCollection stopMocking];
1336 }
1337
1338 - (void)testItReportsAccessibilityOnOffSwitchLabelsFlagNotSet {
1339 if (@available(iOS 13, *)) {
1340 // noop
1341 } else {
1342 return;
1343 }
1344
1345 // Setup test.
1346 FlutterViewController* viewController =
1347 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
1348 id partialMockViewController = OCMPartialMock(viewController);
1349 OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(NO);
1350
1351 // Exercise behavior under test.
1352 int32_t flags = [partialMockViewController accessibilityFlags];
1353
1354 // Verify behavior.
1355 XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) == 0);
1356 }
1357
1358 - (void)testItReportsAccessibilityOnOffSwitchLabelsFlagSet {
1359 if (@available(iOS 13, *)) {
1360 // noop
1361 } else {
1362 return;
1363 }
1364
1365 // Setup test.
1366 FlutterViewController* viewController =
1367 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
1368 id partialMockViewController = OCMPartialMock(viewController);
1369 OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(YES);
1370
1371 // Exercise behavior under test.
1372 int32_t flags = [partialMockViewController accessibilityFlags];
1373
1374 // Verify behavior.
1375 XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) != 0);
1376 }
1377
1378 - (void)testAccessibilityPerformEscapePopsRoute {
1379 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1380 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1381 id mockNavigationChannel = OCMClassMock([FlutterMethodChannel class]);
1382 OCMStub([mockEngine navigationChannel]).andReturn(mockNavigationChannel);
1383
1384 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1385 nibName:nil
1386 bundle:nil];
1387 XCTAssertTrue([viewController accessibilityPerformEscape]);
1388
1389 OCMVerify([mockNavigationChannel invokeMethod:@"popRoute" arguments:nil]);
1390
1391 [mockNavigationChannel stopMocking];
1392 }
1393
1394 - (void)testPerformOrientationUpdateForcesOrientationChange {
1395 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1396 currentOrientation:UIInterfaceOrientationLandscapeLeft
1397 didChangeOrientation:YES
1398 resultingOrientation:UIInterfaceOrientationPortrait];
1399
1400 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1401 currentOrientation:UIInterfaceOrientationLandscapeRight
1402 didChangeOrientation:YES
1403 resultingOrientation:UIInterfaceOrientationPortrait];
1404
1405 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1406 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1407 didChangeOrientation:YES
1408 resultingOrientation:UIInterfaceOrientationPortrait];
1409
1410 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1411 currentOrientation:UIInterfaceOrientationLandscapeLeft
1412 didChangeOrientation:YES
1413 resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1414
1415 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1416 currentOrientation:UIInterfaceOrientationLandscapeRight
1417 didChangeOrientation:YES
1418 resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1419
1420 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1421 currentOrientation:UIInterfaceOrientationPortrait
1422 didChangeOrientation:YES
1423 resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1424
1425 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1426 currentOrientation:UIInterfaceOrientationPortrait
1427 didChangeOrientation:YES
1428 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1429
1430 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1431 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1432 didChangeOrientation:YES
1433 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1434
1435 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1436 currentOrientation:UIInterfaceOrientationPortrait
1437 didChangeOrientation:YES
1438 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1439
1440 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1441 currentOrientation:UIInterfaceOrientationLandscapeRight
1442 didChangeOrientation:YES
1443 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1444
1445 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1446 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1447 didChangeOrientation:YES
1448 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1449
1450 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1451 currentOrientation:UIInterfaceOrientationPortrait
1452 didChangeOrientation:YES
1453 resultingOrientation:UIInterfaceOrientationLandscapeRight];
1454
1455 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1456 currentOrientation:UIInterfaceOrientationLandscapeLeft
1457 didChangeOrientation:YES
1458 resultingOrientation:UIInterfaceOrientationLandscapeRight];
1459
1460 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1461 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1462 didChangeOrientation:YES
1463 resultingOrientation:UIInterfaceOrientationLandscapeRight];
1464
1465 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1466 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1467 didChangeOrientation:YES
1468 resultingOrientation:UIInterfaceOrientationPortrait];
1469 }
1470
1471 - (void)testPerformOrientationUpdateDoesNotForceOrientationChange {
1472 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1473 currentOrientation:UIInterfaceOrientationPortrait
1474 didChangeOrientation:NO
1475 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1476
1477 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1478 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1479 didChangeOrientation:NO
1480 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1481
1482 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1483 currentOrientation:UIInterfaceOrientationLandscapeLeft
1484 didChangeOrientation:NO
1485 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1486
1487 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1488 currentOrientation:UIInterfaceOrientationLandscapeRight
1489 didChangeOrientation:NO
1490 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1491
1492 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1493 currentOrientation:UIInterfaceOrientationPortrait
1494 didChangeOrientation:NO
1495 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1496
1497 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1498 currentOrientation:UIInterfaceOrientationLandscapeLeft
1499 didChangeOrientation:NO
1500 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1501
1502 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1503 currentOrientation:UIInterfaceOrientationLandscapeRight
1504 didChangeOrientation:NO
1505 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1506
1507 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1508 currentOrientation:UIInterfaceOrientationPortrait
1509 didChangeOrientation:NO
1510 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1511
1512 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1513 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1514 didChangeOrientation:NO
1515 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1516
1517 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1518 currentOrientation:UIInterfaceOrientationLandscapeLeft
1519 didChangeOrientation:NO
1520 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1521
1522 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1523 currentOrientation:UIInterfaceOrientationLandscapeRight
1524 didChangeOrientation:NO
1525 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1526
1527 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1528 currentOrientation:UIInterfaceOrientationLandscapeLeft
1529 didChangeOrientation:NO
1530 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1531
1532 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1533 currentOrientation:UIInterfaceOrientationLandscapeRight
1534 didChangeOrientation:NO
1535 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1536 }
1537
1538 // Perform an orientation update test that fails when the expected outcome
1539 // for an orientation update is not met
1540 - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask
1541 currentOrientation:(UIInterfaceOrientation)currentOrientation
1542 didChangeOrientation:(BOOL)didChange
1543 resultingOrientation:(UIInterfaceOrientation)resultingOrientation {
1544 id mockApplication = OCMClassMock([UIApplication class]);
1545 id mockWindowScene;
1546 id deviceMock;
1547 id mockVC;
1548 __block __weak id weakPreferences;
1549 @autoreleasepool {
1550 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1551 nibName:nil
1552 bundle:nil];
1553
1554 if (@available(iOS 16.0, *)) {
1555 mockWindowScene = OCMClassMock([UIWindowScene class]);
1556 mockVC = OCMPartialMock(realVC);
1557 OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1558 if (realVC.supportedInterfaceOrientations == mask) {
1559 OCMReject([mockWindowScene requestGeometryUpdateWithPreferences:[OCMArg any]
1560 errorHandler:[OCMArg any]]);
1561 } else {
1562 // iOS 16 will decide whether to rotate based on the new preference, so always set it
1563 // when it changes.
1564 OCMExpect([mockWindowScene
1565 requestGeometryUpdateWithPreferences:[OCMArg checkWithBlock:^BOOL(
1566 UIWindowSceneGeometryPreferencesIOS*
1567 preferences) {
1568 weakPreferences = preferences;
1569 return preferences.interfaceOrientations == mask;
1570 }]
1571 errorHandler:[OCMArg any]]);
1572 }
1573 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1574 OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockWindowScene]);
1575 } else {
1576 deviceMock = OCMPartialMock([UIDevice currentDevice]);
1577 if (!didChange) {
1578 OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]);
1579 } else {
1580 OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]);
1581 }
1582 if (@available(iOS 13.0, *)) {
1583 mockWindowScene = OCMClassMock([UIWindowScene class]);
1584 mockVC = OCMPartialMock(realVC);
1585 OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1586 OCMStub(((UIWindowScene*)mockWindowScene).interfaceOrientation)
1587 .andReturn(currentOrientation);
1588 } else {
1589 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1590 OCMStub([mockApplication statusBarOrientation]).andReturn(currentOrientation);
1591 }
1592 }
1593
1594 [realVC performOrientationUpdate:mask];
1595 if (@available(iOS 16.0, *)) {
1596 OCMVerifyAll(mockWindowScene);
1597 } else {
1598 OCMVerifyAll(deviceMock);
1599 }
1600 }
1601 [mockWindowScene stopMocking];
1602 [deviceMock stopMocking];
1603 [mockApplication stopMocking];
1604 XCTAssertNil(weakPreferences);
1605 }
1606
1607 // Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
1608 // which is set to the given "contrast".
1609 - (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
1610 id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1611 OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
1612 return mockTraitCollection;
1613 }
1614
1615 - (void)testWillDeallocNotification {
1616 XCTestExpectation* expectation =
1617 [[XCTestExpectation alloc] initWithDescription:@"notification called"];
1618 id engine = [[MockEngine alloc] init];
1619 @autoreleasepool {
1620 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
1621 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1622 nibName:nil
1623 bundle:nil];
1624 [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc
1625 object:nil
1626 queue:[NSOperationQueue mainQueue]
1627 usingBlock:^(NSNotification* _Nonnull note) {
1628 [expectation fulfill];
1629 }];
1630 XCTAssertNotNil(realVC);
1631 realVC = nil;
1632 }
1633 [self waitForExpectations:@[ expectation ] timeout:1.0];
1634 }
1635
1636 - (void)testReleasesKeyboardManagerOnDealloc {
1637 __weak FlutterKeyboardManager* weakKeyboardManager = nil;
1638 @autoreleasepool {
1639 FlutterViewController* viewController = [[FlutterViewController alloc] init];
1640
1641 [viewController addInternalPlugins];
1642 weakKeyboardManager = viewController.keyboardManager;
1643 XCTAssertNotNil(weakKeyboardManager);
1644 [viewController deregisterNotifications];
1645 viewController = nil;
1646 }
1647 // View controller has released the keyboard manager.
1648 XCTAssertNil(weakKeyboardManager);
1649 }
1650
1651 - (void)testDoesntLoadViewInInit {
1652 FlutterDartProject* project = [[FlutterDartProject alloc] init];
1653 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1654 [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1655 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1656 nibName:nil
1657 bundle:nil];
1658 XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown");
1659 engine.viewController = nil;
1660 }
1661
1662 - (void)testHideOverlay {
1663 FlutterDartProject* project = [[FlutterDartProject alloc] init];
1664 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1665 [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1666 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1667 nibName:nil
1668 bundle:nil];
1669 XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @"");
1670 [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator
1671 object:nil];
1672 XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @"");
1673 engine.viewController = nil;
1674 }
1675
1676 - (void)testNotifyLowMemory {
1677 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
1678 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1679 nibName:nil
1680 bundle:nil];
1681 id viewControllerMock = OCMPartialMock(viewController);
1682 OCMStub([viewControllerMock surfaceUpdated:NO]);
1683 [viewController beginAppearanceTransition:NO animated:NO];
1684 [viewController endAppearanceTransition];
1685 XCTAssertTrue(mockEngine.didCallNotifyLowMemory);
1686 }
1687
1688 - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback {
1689 NSMutableDictionary* replyMessage = [@{
1690 @"handled" : @YES,
1691 } mutableCopy];
1692 // Response is async, so we have to post it to the run loop instead of calling
1693 // it directly.
1694 self.messageSent = message;
1695 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
1696 ^() {
1697 callback(replyMessage);
1698 });
1699 }
1700
1701 - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
1702 if (@available(iOS 13.4, *)) {
1703 // noop
1704 } else {
1705 return;
1706 }
1707 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
1708 mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1709 OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1710 .andCall(self, @selector(sendMessage:reply:));
1711 OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1712 mockEngine.textInputPlugin = self.mockTextInputPlugin;
1713
1714 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1715 nibName:nil
1716 bundle:nil];
1717
1718 // Allocate the keyboard manager in the view controller by adding the internal
1719 // plugins.
1720 [vc addInternalPlugins];
1721
1722 [vc handlePressEvent:keyUpEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0)
1723 nextAction:^(){
1724 }];
1725
1726 XCTAssert(self.messageSent != nil);
1727 XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1728 XCTAssert([self.messageSent[@"type"] isEqualToString:@"keyup"]);
1729 XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1730 XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1731 XCTAssert([self.messageSent[@"characters"] isEqualToString:@""]);
1732 XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@""]);
1733 [vc deregisterNotifications];
1734 }
1735
1736 - (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
1737 if (@available(iOS 13.4, *)) {
1738 // noop
1739 } else {
1740 return;
1741 }
1742
1743 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
1744 mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1745 OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1746 .andCall(self, @selector(sendMessage:reply:));
1747 OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1748 mockEngine.textInputPlugin = self.mockTextInputPlugin;
1749
1750 __strong FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1751 nibName:nil
1752 bundle:nil];
1753 // Allocate the keyboard manager in the view controller by adding the internal
1754 // plugins.
1755 [vc addInternalPlugins];
1756
1757 [vc handlePressEvent:keyDownEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0f, "A",
1758 "a")
1759 nextAction:^(){
1760 }];
1761
1762 XCTAssert(self.messageSent != nil);
1763 XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1764 XCTAssert([self.messageSent[@"type"] isEqualToString:@"keydown"]);
1765 XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1766 XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1767 XCTAssert([self.messageSent[@"characters"] isEqualToString:@"A"]);
1768 XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@"a"]);
1769 [vc deregisterNotifications];
1770 vc = nil;
1771 }
1772
1773 - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
1774 if (@available(iOS 13.4, *)) {
1775 // noop
1776 } else {
1777 return;
1778 }
1779 id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1780 OCMStub([keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1781 .andCall(self, @selector(sendMessage:reply:));
1782 OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1783 OCMStub([self.mockEngine keyEventChannel]).andReturn(keyEventChannel);
1784
1785 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1786 nibName:nil
1787 bundle:nil];
1788
1789 // Allocate the keyboard manager in the view controller by adding the internal
1790 // plugins.
1791 [vc addInternalPlugins];
1792
1793 [vc handlePressEvent:keyEventWithPhase(UIPressPhaseStationary, UIKeyboardHIDUsageKeyboardA,
1794 UIKeyModifierShift, 123.0)
1795 nextAction:^(){
1796 }];
1797 [vc handlePressEvent:keyEventWithPhase(UIPressPhaseCancelled, UIKeyboardHIDUsageKeyboardA,
1798 UIKeyModifierShift, 123.0)
1799 nextAction:^(){
1800 }];
1801 [vc handlePressEvent:keyEventWithPhase(UIPressPhaseChanged, UIKeyboardHIDUsageKeyboardA,
1802 UIKeyModifierShift, 123.0)
1803 nextAction:^(){
1804 }];
1805
1806 XCTAssert(self.messageSent == nil);
1807 OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);
1808 [vc deregisterNotifications];
1809 }
1810
1811 - (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) {
1812 if (@available(iOS 13.4, *)) {
1813 // noop
1814 } else {
1815 return;
1816 }
1817
1818 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1819 nibName:nil
1820 bundle:nil];
1821 XCTAssertNotNil(vc);
1822 UIView* view = vc.view;
1823 XCTAssertNotNil(view);
1824 NSArray* gestureRecognizers = view.gestureRecognizers;
1825 XCTAssertNotNil(gestureRecognizers);
1826
1827 BOOL found = NO;
1828 for (id gesture in gestureRecognizers) {
1829 if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
1830 found = YES;
1831 break;
1832 }
1833 }
1834 XCTAssertTrue(found);
1835 }
1836
1837 - (void)testMouseSupport API_AVAILABLE(ios(13.4)) {
1838 if (@available(iOS 13.4, *)) {
1839 // noop
1840 } else {
1841 return;
1842 }
1843
1844 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1845 nibName:nil
1846 bundle:nil];
1847 XCTAssertNotNil(vc);
1848
1849 id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]);
1850 XCTAssertNotNil(mockPanGestureRecognizer);
1851
1852 [vc discreteScrollEvent:mockPanGestureRecognizer];
1853
1854 [[[self.mockEngine verify] ignoringNonObjectArgs]
1855 dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>(0)];
1856 }
1857
1858 - (void)testFakeEventTimeStamp {
1859 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1860 nibName:nil
1861 bundle:nil];
1862 XCTAssertNotNil(vc);
1863
1864 flutter::PointerData pointer_data = [vc generatePointerDataForFake];
1865 int64_t current_micros = [[NSProcessInfo processInfo] systemUptime] * 1000 * 1000;
1866 int64_t interval_micros = current_micros - pointer_data.time_stamp;
1867 const int64_t tolerance_millis = 2;
1868 XCTAssertTrue(interval_micros / 1000 < tolerance_millis,
1869 @"PointerData.time_stamp should be equal to NSProcessInfo.systemUptime");
1870 }
1871
1872 - (void)testSplashScreenViewCanSetNil {
1873 FlutterViewController* flutterViewController =
1874 [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
1875 [flutterViewController setSplashScreenView:nil];
1876 }
1877
1878 - (void)testLifeCycleNotificationBecameActive {
1879 FlutterEngine* engine = [[FlutterEngine alloc] init];
1880 [engine runWithEntrypoint:nil];
1881 FlutterViewController* flutterViewController =
1882 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1883 UIWindow* window = [[UIWindow alloc] init];
1884 [window addSubview:flutterViewController.view];
1885 flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100);
1886 [flutterViewController viewDidLayoutSubviews];
1887 NSNotification* sceneNotification =
1888 [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil];
1889 NSNotification* applicationNotification =
1890 [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
1891 object:nil
1892 userInfo:nil];
1893 id mockVC = OCMPartialMock(flutterViewController);
1894 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1895 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1896 #if APPLICATION_EXTENSION_API_ONLY
1897 OCMVerify([mockVC sceneBecameActive:[OCMArg any]]);
1898 OCMReject([mockVC applicationBecameActive:[OCMArg any]]);
1899 #else
1900 OCMReject([mockVC sceneBecameActive:[OCMArg any]]);
1901 OCMVerify([mockVC applicationBecameActive:[OCMArg any]]);
1902 #endif
1903 XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1904 OCMVerify([mockVC surfaceUpdated:YES]);
1905 XCTestExpectation* timeoutApplicationLifeCycle =
1906 [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
1907 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
1908 dispatch_get_main_queue(), ^{
1909 [timeoutApplicationLifeCycle fulfill];
1910 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
1911 [flutterViewController deregisterNotifications];
1912 });
1913 [self waitForExpectationsWithTimeout:5.0 handler:nil];
1914 }
1915
1916 - (void)testLifeCycleNotificationWillResignActive {
1917 FlutterEngine* engine = [[FlutterEngine alloc] init];
1918 [engine runWithEntrypoint:nil];
1919 FlutterViewController* flutterViewController =
1920 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1921 NSNotification* sceneNotification =
1922 [NSNotification notificationWithName:UISceneWillDeactivateNotification
1923 object:nil
1924 userInfo:nil];
1925 NSNotification* applicationNotification =
1926 [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
1927 object:nil
1928 userInfo:nil];
1929 id mockVC = OCMPartialMock(flutterViewController);
1930 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1931 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1932 #if APPLICATION_EXTENSION_API_ONLY
1933 OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]);
1934 OCMReject([mockVC applicationWillResignActive:[OCMArg any]]);
1935 #else
1936 OCMReject([mockVC sceneWillResignActive:[OCMArg any]]);
1937 OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]);
1938 #endif
1939 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
1940 [flutterViewController deregisterNotifications];
1941 }
1942
1943 - (void)testLifeCycleNotificationWillTerminate {
1944 FlutterEngine* engine = [[FlutterEngine alloc] init];
1945 [engine runWithEntrypoint:nil];
1946 FlutterViewController* flutterViewController =
1947 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1948 NSNotification* sceneNotification =
1949 [NSNotification notificationWithName:UISceneDidDisconnectNotification
1950 object:nil
1951 userInfo:nil];
1952 NSNotification* applicationNotification =
1953 [NSNotification notificationWithName:UIApplicationWillTerminateNotification
1954 object:nil
1955 userInfo:nil];
1956 id mockVC = OCMPartialMock(flutterViewController);
1957 id mockEngine = OCMPartialMock(engine);
1958 OCMStub([mockVC engine]).andReturn(mockEngine);
1959 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1960 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1961 #if APPLICATION_EXTENSION_API_ONLY
1962 OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]);
1963 OCMReject([mockVC applicationWillTerminate:[OCMArg any]]);
1964 #else
1965 OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]);
1966 OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]);
1967 #endif
1968 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]);
1969 OCMVerify([mockEngine destroyContext]);
1970 [flutterViewController deregisterNotifications];
1971 }
1972
1973 - (void)testLifeCycleNotificationDidEnterBackground {
1974 FlutterEngine* engine = [[FlutterEngine alloc] init];
1975 [engine runWithEntrypoint:nil];
1976 FlutterViewController* flutterViewController =
1977 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1978 NSNotification* sceneNotification =
1979 [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
1980 object:nil
1981 userInfo:nil];
1982 NSNotification* applicationNotification =
1983 [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
1984 object:nil
1985 userInfo:nil];
1986 id mockVC = OCMPartialMock(flutterViewController);
1987 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1988 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1989 #if APPLICATION_EXTENSION_API_ONLY
1990 OCMVerify([mockVC sceneDidEnterBackground:[OCMArg any]]);
1991 OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]);
1992 #else
1993 OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]);
1994 OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]);
1995 #endif
1996 XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1997 OCMVerify([mockVC surfaceUpdated:NO]);
1998 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]);
1999 [flutterViewController deregisterNotifications];
2000 }
2001
2002 - (void)testLifeCycleNotificationWillEnterForeground {
2003 FlutterEngine* engine = [[FlutterEngine alloc] init];
2004 [engine runWithEntrypoint:nil];
2005 FlutterViewController* flutterViewController =
2006 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2007 NSNotification* sceneNotification =
2008 [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
2009 object:nil
2010 userInfo:nil];
2011 NSNotification* applicationNotification =
2012 [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
2013 object:nil
2014 userInfo:nil];
2015 id mockVC = OCMPartialMock(flutterViewController);
2016 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
2017 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
2018 #if APPLICATION_EXTENSION_API_ONLY
2019 OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]);
2020 OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]);
2021 #else
2022 OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]);
2023 OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]);
2024 #endif
2025 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2026 [flutterViewController deregisterNotifications];
2027 }
2028
2029 - (void)testLifeCycleNotificationCancelledInvalidResumed {
2030 FlutterEngine* engine = [[FlutterEngine alloc] init];
2031 [engine runWithEntrypoint:nil];
2032 FlutterViewController* flutterViewController =
2033 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2034 NSNotification* applicationDidBecomeActiveNotification =
2035 [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
2036 object:nil
2037 userInfo:nil];
2038 NSNotification* applicationWillResignActiveNotification =
2039 [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
2040 object:nil
2041 userInfo:nil];
2042 id mockVC = OCMPartialMock(flutterViewController);
2043 [[NSNotificationCenter defaultCenter] postNotification:applicationDidBecomeActiveNotification];
2044 [[NSNotificationCenter defaultCenter] postNotification:applicationWillResignActiveNotification];
2045 #if APPLICATION_EXTENSION_API_ONLY
2046 #else
2047 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2048 #endif
2049
2050 XCTestExpectation* timeoutApplicationLifeCycle =
2051 [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
2052 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
2053 dispatch_get_main_queue(), ^{
2054 OCMReject([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
2055 [timeoutApplicationLifeCycle fulfill];
2056 [flutterViewController deregisterNotifications];
2057 });
2058 [self waitForExpectationsWithTimeout:5.0 handler:nil];
2059 }
2060
2061 - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterViewController {
2062 id bundleMock = OCMPartialMock([NSBundle mainBundle]);
2063 OCMStub([bundleMock objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"])
2064 .andReturn(@YES);
2065 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2066 double maxFrameRate = 120;
2067 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2068 FlutterEngine* engine = [[FlutterEngine alloc] init];
2069 [engine runWithEntrypoint:nil];
2070 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2071 nibName:nil
2072 bundle:nil];
2073 FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
2074 };
2075 [viewController setUpKeyboardAnimationVsyncClient:callback];
2076 XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2077 CADisplayLink* link = [viewController.keyboardAnimationVSyncClient getDisplayLink];
2078 XCTAssertNotNil(link);
2079 if (@available(iOS 15.0, *)) {
2080 XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate);
2081 XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate);
2082 XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2);
2083 } else {
2084 XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate);
2085 }
2086 }
2087
2088 - (void)
2089 testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
2090 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2091 double maxFrameRate = 120;
2092 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2093 FlutterEngine* engine = [[FlutterEngine alloc] init];
2094 [engine runWithEntrypoint:nil];
2095 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2096 nibName:nil
2097 bundle:nil];
2098 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2099 XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
2100 }
2101
2102 - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
2103 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2104 double maxFrameRate = 120;
2105 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2106
2107 FlutterEngine* engine = [[FlutterEngine alloc] init];
2108 [engine runWithEntrypoint:nil];
2109 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2110 nibName:nil
2111 bundle:nil];
2112 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2113 VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
2114 XCTAssertNotNil(clientBefore);
2115
2116 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2117 VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
2118 XCTAssertNotNil(clientAfter);
2119
2120 XCTAssertTrue(clientBefore == clientAfter);
2121 }
2122
2123 - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
2124 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2125 double maxFrameRate = 60;
2126 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2127 FlutterEngine* engine = [[FlutterEngine alloc] init];
2128 [engine runWithEntrypoint:nil];
2129 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2130 nibName:nil
2131 bundle:nil];
2132 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2133 XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
2134 }
2135
2136 - (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
2137 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2138 double maxFrameRate = 120;
2139 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2140 FlutterEngine* engine = [[FlutterEngine alloc] init];
2141 [engine runWithEntrypoint:nil];
2142 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2143 nibName:nil
2144 bundle:nil];
2145 [viewController loadView];
2146 [viewController viewDidLoad];
2147
2148 VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
2149 CADisplayLink* link = [client getDisplayLink];
2150
2151 UITouch* fakeTouchBegan = [[UITouch alloc] init];
2152 fakeTouchBegan.phase = UITouchPhaseBegan;
2153
2154 UITouch* fakeTouchMove = [[UITouch alloc] init];
2155 fakeTouchMove.phase = UITouchPhaseMoved;
2156
2157 UITouch* fakeTouchEnd = [[UITouch alloc] init];
2158 fakeTouchEnd.phase = UITouchPhaseEnded;
2159
2160 UITouch* fakeTouchCancelled = [[UITouch alloc] init];
2161 fakeTouchCancelled.phase = UITouchPhaseCancelled;
2162
2163 [viewController
2164 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
2165 XCTAssertFalse(link.isPaused);
2166
2167 [viewController
2168 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
2169 XCTAssertTrue(link.isPaused);
2170
2171 [viewController
2172 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
2173 XCTAssertFalse(link.isPaused);
2174
2175 [viewController
2176 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
2177 XCTAssertTrue(link.isPaused);
2178
2179 [viewController
2180 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2181 initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
2182 XCTAssertFalse(link.isPaused);
2183
2184 [viewController
2185 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
2186 fakeTouchCancelled, nil]];
2187 XCTAssertTrue(link.isPaused);
2188
2189 [viewController
2190 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2191 initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
2192 XCTAssertFalse(link.isPaused);
2193 }
2194
2195 - (void)testFlutterViewControllerStartKeyboardAnimationWillCreateVsyncClientCorrectly {
2196 FlutterEngine* engine = [[FlutterEngine alloc] init];
2197 [engine runWithEntrypoint:nil];
2198 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2199 nibName:nil
2200 bundle:nil];
2201 viewController.targetViewInsetBottom = 100;
2202 [viewController startKeyBoardAnimation:0.25];
2203 XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2204 }
2205
2206 - (void)
2207 testSetupKeyboardAnimationVsyncClientWillNotCreateNewVsyncClientWhenKeyboardAnimationCallbackIsNil {
2208 FlutterEngine* engine = [[FlutterEngine alloc] init];
2209 [engine runWithEntrypoint:nil];
2210 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2211 nibName:nil
2212 bundle:nil];
2213 [viewController setUpKeyboardAnimationVsyncClient:nil];
2214 XCTAssertNil(viewController.keyboardAnimationVSyncClient);
2215 }
2216
2217 - (void)testSupportsShowingSystemContextMenuForIOS16AndAbove {
2218 FlutterEngine* engine = [[FlutterEngine alloc] init];
2219 [engine runWithEntrypoint:nil];
2220 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2221 nibName:nil
2222 bundle:nil];
2223 BOOL supportsShowingSystemContextMenu = [viewController supportsShowingSystemContextMenu];
2224 if (@available(iOS 16.0, *)) {
2225 XCTAssertTrue(supportsShowingSystemContextMenu);
2226 } else {
2227 XCTAssertFalse(supportsShowingSystemContextMenu);
2228 }
2229 }
2230
2231 @end
-[FlutterViewController(Tests) invalidateKeyboardAnimationVSyncClient]
void invalidateKeyboardAnimationVSyncClient()
FlutterEnginePartialMock::lifecycleChannel
FlutterBasicMessageChannel * lifecycleChannel
Definition: FlutterViewControllerTest.mm:40
FlutterEnginePartialMock
Definition: FlutterViewControllerTest.mm:39
FlutterEngine
Definition: FlutterEngine.h:61
FlutterBasicMessageChannel
Definition: FlutterChannels.h:37
FlutterViewController
Definition: FlutterViewController.h:56
FlutterMethodChannel
Definition: FlutterChannels.h:220
-[FlutterViewController(Tests) updateViewportMetricsIfNeeded]
void updateViewportMetricsIfNeeded()
-[FlutterEngine runWithEntrypoint:]
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
FlutterViewController(Tests)::targetViewInsetBottom
double targetViewInsetBottom
Definition: FlutterViewControllerTest.mm:124
FlutterTextInputPlugin.h
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
FlutterViewController(Tests)::keyboardAnimationIsShowing
BOOL keyboardAnimationIsShowing
Definition: FlutterViewControllerTest.mm:126
-[VSyncClient(Testing) getDisplayLink]
CADisplayLink * getDisplayLink()
DisplayLinkManager
Definition: FlutterMetalLayer.mm:16
FlutterViewController(Tests)::touchRateCorrectionVSyncClient
VSyncClient * touchRateCorrectionVSyncClient
Definition: FlutterViewControllerTest.mm:128
FlutterViewControllerTest
Definition: FlutterViewControllerTest.mm:170
flutter::testing
Definition: FlutterFakeKeyEvents.h:51
FlutterMacros.h
FlutterEmbedderKeyResponder.h
FlutterSendKeyEvent
void(^ FlutterSendKeyEvent)(const FlutterKeyEvent &, _Nullable FlutterKeyEventCallback, void *_Nullable)
Definition: FlutterEmbedderKeyResponder.h:13
FlutterViewControllerTest::messageSent
id messageSent
Definition: FlutterViewControllerTest.mm:173
viewController
FlutterViewController * viewController
Definition: FlutterTextInputPluginTest.mm:92
FlutterEmbedderKeyResponder(Tests)::sendEvent
FlutterSendKeyEvent sendEvent
Definition: FlutterViewControllerTest.mm:119
FlutterKeyboardAnimationCallback
void(^ FlutterKeyboardAnimationCallback)(fml::TimePoint)
Definition: FlutterViewController_Internal.h:42
FlutterViewControllerTest::mockTextInputPlugin
id mockTextInputPlugin
Definition: FlutterViewControllerTest.mm:172
FlutterEngine(TestLowMemory)
Definition: FlutterViewControllerTest.mm:86
FlutterFakeKeyEvents.h
-[FlutterEngine(TestLowMemory) notifyLowMemory]
void notifyLowMemory()
flutter
Definition: accessibility_bridge.h:28
-[FlutterViewController(Tests) addInternalPlugins]
void addInternalPlugins()
FlutterBinaryMessenger.h
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:33
FlutterEnginePartialMock::keyEventChannel
FlutterBasicMessageChannel * keyEventChannel
Definition: FlutterViewControllerTest.mm:41
FlutterViewControllerTest::mockEngine
id mockEngine
Definition: FlutterViewControllerTest.mm:171
-[FlutterViewController(Tests) keyboardAnimationView]
UIView * keyboardAnimationView()
UIViewController+FlutterScreenAndSceneIfLoaded.h
FlutterViewController(Tests)::keyboardAnimationVSyncClient
VSyncClient * keyboardAnimationVSyncClient
Definition: FlutterViewControllerTest.mm:127
FlutterViewController(Tests)
Definition: FlutterViewControllerTest.mm:122
-[FlutterViewController(Tests) keyboardSpringAnimation]
SpringAnimation * keyboardSpringAnimation()
FlutterReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
engine
id engine
Definition: FlutterTextInputPluginTest.mm:89
textInputPlugin
FlutterTextInputPlugin * textInputPlugin
Definition: FlutterTextInputPluginTest.mm:90
FlutterViewController_Internal.h
FlutterKeyboardManager(Tests)
Definition: FlutterViewControllerTest.mm:113
FlutterUIPressProxy
Definition: FlutterUIPressProxy.h:17
MockEngine
Definition: FlutterViewControllerTest.mm:100
FlutterEmbedderKeyResponder(Tests)
Definition: FlutterViewControllerTest.mm:118
FlutterView
Definition: FlutterView.h:34
VSyncClient(Testing)
Definition: FlutterViewControllerTest.mm:183
-[FlutterEngine destroyContext]
void destroyContext()
Definition: FlutterEngine.mm:479
vsync_waiter_ios.h
FlutterKeyboardManager(Tests)::primaryResponders
NSMutableArray< id< FlutterKeyPrimaryResponder > > * primaryResponders
Definition: FlutterViewControllerTest.mm:115
FlutterViewController(Tests)::isKeyboardInOrTransitioningFromBackground
BOOL isKeyboardInOrTransitioningFromBackground
Definition: FlutterViewControllerTest.mm:125
FlutterViewController::binaryMessenger
NSObject< FlutterBinaryMessenger > * binaryMessenger
Definition: FlutterViewController.h:243
FlutterDartProject
Definition: FlutterDartProject.mm:262
-[FlutterViewController(Tests) ensureViewportMetricsIsCorrect]
void ensureViewportMetricsIsCorrect()
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:53
-[FlutterViewController(Tests) createTouchRateCorrectionVSyncClientIfNeeded]
void createTouchRateCorrectionVSyncClientIfNeeded()
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
FlutterEmbedderKeyResponder
Definition: FlutterEmbedderKeyResponder.h:23
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
VSyncClient
Definition: vsync_waiter_ios.h:36
FlutterView.h
FlutterViewController.h
FlutterViewControllerWillDealloc
const NSNotificationName FlutterViewControllerWillDealloc
Definition: FlutterViewController.mm:43