From 16c2058bf21560aee224bd8a19aac8f697216474 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 14 Aug 2019 09:10:22 -0700 Subject: [PATCH] Add isDisplayingFlutterUI to FlutterViewController (flutter/engine#10816) --- .../framework/Headers/FlutterViewController.h | 8 +++++++ .../framework/Source/FlutterViewController.mm | 22 +++++++++++++++++ .../FlutterViewControllerTest.m | 24 ++++++++++++------- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 90588ac9f92..fc2687c4456 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -130,6 +130,14 @@ FLUTTER_EXPORT */ - (id)pluginRegistry; +/** + * True if at least one frame has rendered and the ViewController has appeared. + * + * This property is reset to false when the ViewController disappears. It is + * guaranteed to only alternate between true and false for observers. + */ +@property(nonatomic, readonly, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI; + /** * Specifies the view to use as a splash screen. Flutter's rendering is asynchronous, so the first * frame rendered by the Flutter application might not immediately appear when theFlutter view is diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index f94593913b1..74f604d69fa 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -29,6 +29,7 @@ NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemantics // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are // just a warning. @interface FlutterViewController () +@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI; @end @implementation FlutterViewController { @@ -49,6 +50,8 @@ NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemantics NSMutableSet* _ongoingTouches; } +@synthesize displayingFlutterUI = _displayingFlutterUI; + #pragma mark - Manage and override all designated initializers - (instancetype)initWithEngine:(FlutterEngine*)engine @@ -263,7 +266,25 @@ NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemantics [self.view addSubview:splashScreenView]; } ++ (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI { + return NO; +} + +- (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI { + if (_displayingFlutterUI != displayingFlutterUI) { + if (displayingFlutterUI == YES) { + if (!self.isViewLoaded || !self.view.window) { + return; + } + } + [self willChangeValueForKey:@"displayingFlutterUI"]; + _displayingFlutterUI = displayingFlutterUI; + [self didChangeValueForKey:@"displayingFlutterUI"]; + } +} + - (void)callViewRenderedCallback { + self.displayingFlutterUI = YES; if (_flutterViewRenderedCallback != nil) { _flutterViewRenderedCallback.get()(); _flutterViewRenderedCallback.reset(); @@ -395,6 +416,7 @@ NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemantics [_engine.get() platformViewsController] -> SetFlutterViewController(self); [_engine.get() platformView] -> NotifyCreated(); } else { + self.displayingFlutterUI = NO; [_engine.get() platformView] -> NotifyDestroyed(); [_engine.get() platformViewsController] -> SetFlutterView(nullptr); [_engine.get() platformViewsController] -> SetFlutterViewController(nullptr); diff --git a/engine/src/flutter/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m b/engine/src/flutter/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m index 833f8a6afcb..15acf3fcd79 100644 --- a/engine/src/flutter/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m +++ b/engine/src/flutter/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m @@ -25,25 +25,31 @@ } - (void)testFirstFrameCallback { + XCTestExpectation* firstFrameRendered = [self expectationWithDescription:@"firstFrameRendered"]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; [engine runWithEntrypoint:nil]; self.flutterViewController = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - __block BOOL shouldKeepRunning = YES; + + XCTAssertFalse(self.flutterViewController.isDisplayingFlutterUI); + + XCTestExpectation* displayingFlutterUIExpectation = + [self keyValueObservingExpectationForObject:self.flutterViewController + keyPath:@"displayingFlutterUI" + expectedValue:@YES]; + displayingFlutterUIExpectation.assertForOverFulfill = YES; + [self.flutterViewController setFlutterViewDidRenderCallback:^{ - shouldKeepRunning = NO; + [firstFrameRendered fulfill]; }]; + AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; UIViewController* rootVC = appDelegate.window.rootViewController; [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; - NSRunLoop* runLoop = [NSRunLoop currentRunLoop]; - int countDownMs = 2000; - while (shouldKeepRunning && countDownMs > 0) { - [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - countDownMs -= 100; - } - XCTAssertGreaterThan(countDownMs, 0); + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; } @end