mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Make iOS FlutterViewController stop sending inactive/pause on app lifecycle events when not visible (#12128)
This commit is contained in:
parent
709fc6e00c
commit
4159c2bfed
@ -537,23 +537,32 @@ typedef enum UIAccessibilityContrast : NSInteger {
|
||||
TRACE_EVENT0("flutter", "applicationBecameActive");
|
||||
if (_viewportMetrics.physical_width)
|
||||
[self surfaceUpdated:YES];
|
||||
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
|
||||
[self goToApplicationLifecycle:@"AppLifecycleState.resumed"];
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification*)notification {
|
||||
TRACE_EVENT0("flutter", "applicationWillResignActive");
|
||||
[self surfaceUpdated:NO];
|
||||
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
|
||||
[self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(NSNotification*)notification {
|
||||
TRACE_EVENT0("flutter", "applicationDidEnterBackground");
|
||||
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
|
||||
[self goToApplicationLifecycle:@"AppLifecycleState.paused"];
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(NSNotification*)notification {
|
||||
TRACE_EVENT0("flutter", "applicationWillEnterForeground");
|
||||
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
|
||||
[self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
|
||||
}
|
||||
|
||||
// Make this transition only while this current view controller is visible.
|
||||
- (void)goToApplicationLifecycle:(nonnull NSString*)state {
|
||||
// Accessing self.view will create the view. Check whether the view is organically loaded
|
||||
// first before checking whether the view is attached to window.
|
||||
if (self.isViewLoaded && self.view.window) {
|
||||
[[_engine.get() lifecycleChannel] sendMessage:state];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Touch event handling
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
@implementation ScreenBeforeFlutter
|
||||
|
||||
FlutterEngine* _engine;
|
||||
@synthesize engine = _engine;
|
||||
|
||||
- (id)initWithEngineRunCompletion:(void (^)(void))engineRunCompletion {
|
||||
self = [super init];
|
||||
|
||||
@ -6,6 +6,28 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#import "ScreenBeforeFlutter.h"
|
||||
|
||||
@interface XCAppLifecycleTestExpectation : XCTestExpectation
|
||||
|
||||
- (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step;
|
||||
@property(nonatomic, readonly, copy) NSString* expectedLifecycle;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XCAppLifecycleTestExpectation
|
||||
|
||||
@synthesize expectedLifecycle = _expectedLifecycle;
|
||||
- (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step {
|
||||
// The step is here because the callbacks into the handler which checks these expectations isn't
|
||||
// synchronous with the executions in the test, so it's hard to find the cause in the test
|
||||
// otherwise.
|
||||
self = [super initWithDescription:[NSString stringWithFormat:@"Expected state %@ during step %@",
|
||||
expectedLifecycle, step]];
|
||||
_expectedLifecycle = [expectedLifecycle copy];
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface AppLifecycleTests : XCTestCase
|
||||
@end
|
||||
|
||||
@ -16,7 +38,7 @@
|
||||
self.continueAfterFailure = NO;
|
||||
}
|
||||
|
||||
- (void)testLifecycleChannel {
|
||||
- (void)testDismissedFlutterViewControllerNotRespondingToApplicationLifecycle {
|
||||
XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"];
|
||||
|
||||
// Let the engine finish booting (at the end of which the channels are properly set-up) before
|
||||
@ -32,121 +54,173 @@
|
||||
FlutterEngine* engine = rootVC.engine;
|
||||
|
||||
NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10];
|
||||
NSMutableArray* lifecycleEvents = [NSMutableArray arrayWithCapacity:10];
|
||||
|
||||
[lifecycleExpectations addObject:[[XCTestExpectation alloc]
|
||||
initWithDescription:@"A loading FlutterViewController goes "
|
||||
@"through AppLifecycleState.inactive"]];
|
||||
[lifecycleExpectations
|
||||
addObject:[[XCTestExpectation alloc]
|
||||
initWithDescription:
|
||||
@"A loading FlutterViewController goes through AppLifecycleState.resumed"]];
|
||||
// Expected sequence from showing the FlutterViewController is inactive and resumed.
|
||||
[lifecycleExpectations addObjectsFromArray:@[
|
||||
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
|
||||
forStep:@"showing a FlutterViewController"],
|
||||
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed"
|
||||
forStep:@"showing a FlutterViewController"]
|
||||
]];
|
||||
|
||||
// Holding onto this FlutterViewController is consequential here. Since a released
|
||||
// FlutterViewController wouldn't keep listening to the application lifecycle events and produce
|
||||
// false positives for the application lifecycle tests further below.
|
||||
FlutterViewController* flutterVC = [rootVC showFlutter];
|
||||
[engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) {
|
||||
if (lifecycleExpectations.count == 0) {
|
||||
XCTFail(@"Unexpected lifecycle transition: %@", message);
|
||||
return;
|
||||
}
|
||||
[lifecycleEvents addObject:message];
|
||||
[[lifecycleExpectations objectAtIndex:0] fulfill];
|
||||
XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0];
|
||||
if (![[nextExpectation expectedLifecycle] isEqualToString:message]) {
|
||||
XCTFail(@"Expected lifecycle %@ but instead received %@", [nextExpectation expectedLifecycle],
|
||||
message);
|
||||
return;
|
||||
}
|
||||
|
||||
[nextExpectation fulfill];
|
||||
[lifecycleExpectations removeObjectAtIndex:0];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5];
|
||||
|
||||
// Expected sequence from showing the FlutterViewController is inactive and resumed.
|
||||
NSArray* expectedStates = @[ @"AppLifecycleState.inactive", @"AppLifecycleState.resumed" ];
|
||||
XCTAssertEqualObjects(lifecycleEvents, expectedStates,
|
||||
@"AppLifecycleState transitions while presenting not as expected");
|
||||
// The expectations list isn't dequeued by the message handler yet.
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];
|
||||
|
||||
// Now dismiss the FlutterViewController again and expect another inactive and paused.
|
||||
[lifecycleExpectations
|
||||
addObject:[[XCTestExpectation alloc]
|
||||
initWithDescription:@"A dismissed FlutterViewController goes through "
|
||||
@"AppLifecycleState.inactive"]];
|
||||
[lifecycleExpectations
|
||||
addObject:[[XCTestExpectation alloc]
|
||||
initWithDescription:@"A dismissed FlutterViewController goes through "
|
||||
@"AppLifecycleState.paused"]];
|
||||
[lifecycleExpectations addObjectsFromArray:@[
|
||||
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
|
||||
forStep:@"dismissing a FlutterViewController"],
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.paused"
|
||||
forStep:@"dismissing a FlutterViewController"]
|
||||
]];
|
||||
[flutterVC dismissViewControllerAnimated:NO completion:nil];
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5];
|
||||
expectedStates = @[
|
||||
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive",
|
||||
@"AppLifecycleState.paused"
|
||||
];
|
||||
XCTAssertEqualObjects(lifecycleEvents, expectedStates,
|
||||
@"AppLifecycleState transitions while dismissing not as expected");
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];
|
||||
|
||||
// Now put the app in the background (while the engine is still running) and bring it back to
|
||||
// the foreground. Granted, we're not winning any awards for hyper-realism but at least we're
|
||||
// checking that we aren't observing the UIApplication notifications and double registering
|
||||
// for AppLifecycleState events.
|
||||
|
||||
// However the production is temporarily wrong. https://github.com/flutter/flutter/issues/37226.
|
||||
// It will be fixed in a next PR that removes the wrong asserts.
|
||||
[lifecycleExpectations
|
||||
addObject:
|
||||
[[XCTestExpectation alloc]
|
||||
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
|
||||
// These operations are synchronous so if they trigger any lifecycle events, they should trigger
|
||||
// failures in the message handler immediately.
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
[lifecycleExpectations
|
||||
addObject:
|
||||
[[XCTestExpectation alloc]
|
||||
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationDidEnterBackgroundNotification
|
||||
object:nil];
|
||||
[lifecycleExpectations
|
||||
addObject:
|
||||
[[XCTestExpectation alloc]
|
||||
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationWillEnterForegroundNotification
|
||||
object:nil];
|
||||
[lifecycleExpectations
|
||||
addObject:
|
||||
[[XCTestExpectation alloc]
|
||||
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
|
||||
// There's no timing latch for our semi-fake background-foreground cycle so launch the
|
||||
// FlutterViewController again to check the complete event list again.
|
||||
[lifecycleExpectations addObject:[[XCTestExpectation alloc]
|
||||
initWithDescription:@"A second FlutterViewController goes "
|
||||
@"through AppLifecycleState.inactive"]];
|
||||
[lifecycleExpectations
|
||||
addObject:[[XCTestExpectation alloc]
|
||||
initWithDescription:
|
||||
@"A second FlutterViewController goes through AppLifecycleState.resumed"]];
|
||||
|
||||
// Expect only lifecycle events from showing the FlutterViewController again, not from any
|
||||
// backgrounding/foregrounding.
|
||||
[lifecycleExpectations addObjectsFromArray:@[
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.inactive"
|
||||
forStep:@"showing a FlutterViewController a second time after backgrounding"],
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.resumed"
|
||||
forStep:@"showing a FlutterViewController a second time after backgrounding"]
|
||||
]];
|
||||
flutterVC = [rootVC showFlutter];
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5];
|
||||
expectedStates = @[
|
||||
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive",
|
||||
@"AppLifecycleState.paused",
|
||||
|
||||
// The production code currently misbehaves. https://github.com/flutter/flutter/issues/37226.
|
||||
// It will be fixed in a next PR that removes the wrong asserts.
|
||||
@"AppLifecycleState.inactive", @"AppLifecycleState.paused", @"AppLifecycleState.inactive",
|
||||
@"AppLifecycleState.resumed",
|
||||
|
||||
// We only added 2 from re-launching the FlutterViewController
|
||||
// and none from the background-foreground cycle.
|
||||
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed"
|
||||
];
|
||||
XCTAssertEqualObjects(
|
||||
lifecycleEvents, expectedStates,
|
||||
@"AppLifecycleState transitions while presenting a second time not as expected");
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];
|
||||
|
||||
// Dismantle.
|
||||
[engine.lifecycleChannel setMessageHandler:nil];
|
||||
[flutterVC dismissViewControllerAnimated:NO completion:nil];
|
||||
flutterVC = nil;
|
||||
[engine setViewController:nil];
|
||||
[rootVC dismissViewControllerAnimated:NO completion:nil];
|
||||
rootVC = nil;
|
||||
}
|
||||
|
||||
- (void)testVisibleFlutterViewControllerRespondsToApplicationLifecycle {
|
||||
XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"];
|
||||
|
||||
// Let the engine finish booting (at the end of which the channels are properly set-up) before
|
||||
// moving onto the next step of showing the next view controller.
|
||||
ScreenBeforeFlutter* rootVC = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:^void() {
|
||||
[engineStartedExpectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectationsWithTimeout:5 handler:nil];
|
||||
|
||||
UIApplication* application = UIApplication.sharedApplication;
|
||||
application.delegate.window.rootViewController = rootVC;
|
||||
FlutterEngine* engine = rootVC.engine;
|
||||
|
||||
NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10];
|
||||
|
||||
// Expected sequence from showing the FlutterViewController is inactive and resumed.
|
||||
[lifecycleExpectations addObjectsFromArray:@[
|
||||
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
|
||||
forStep:@"showing a FlutterViewController"],
|
||||
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed"
|
||||
forStep:@"showing a FlutterViewController"]
|
||||
]];
|
||||
|
||||
FlutterViewController* flutterVC = [rootVC showFlutter];
|
||||
[engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) {
|
||||
if (lifecycleExpectations.count == 0) {
|
||||
XCTFail(@"Unexpected lifecycle transition: %@", message);
|
||||
return;
|
||||
}
|
||||
XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0];
|
||||
if (![[nextExpectation expectedLifecycle] isEqualToString:message]) {
|
||||
XCTFail(@"Expected lifecycle %@ but instead received %@", [nextExpectation expectedLifecycle],
|
||||
message);
|
||||
return;
|
||||
}
|
||||
|
||||
[nextExpectation fulfill];
|
||||
[lifecycleExpectations removeObjectAtIndex:0];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5];
|
||||
|
||||
// Now put the FlutterViewController into background.
|
||||
[lifecycleExpectations addObjectsFromArray:@[
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.inactive"
|
||||
forStep:@"putting FlutterViewController to the background"],
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.paused"
|
||||
forStep:@"putting FlutterViewController to the background"]
|
||||
]];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationDidEnterBackgroundNotification
|
||||
object:nil];
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5];
|
||||
|
||||
// Now restore to foreground
|
||||
[lifecycleExpectations addObjectsFromArray:@[
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.inactive"
|
||||
forStep:@"putting FlutterViewController back to foreground"],
|
||||
[[XCAppLifecycleTestExpectation alloc]
|
||||
initForLifecycle:@"AppLifecycleState.resumed"
|
||||
forStep:@"putting FlutterViewController back to foreground"]
|
||||
]];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationWillEnterForegroundNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:UIApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
[self waitForExpectations:lifecycleExpectations timeout:5];
|
||||
|
||||
// Dismantle.
|
||||
[engine.lifecycleChannel setMessageHandler:nil];
|
||||
[flutterVC dismissViewControllerAnimated:NO completion:nil];
|
||||
[engine setViewController:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user