Skip to content

☂️ Simplify UIScene Lifecycle #185690

Description

@hellohuanlin

Use case

Background:

For plugins to receive scene lifecycle delegate calls, we maintain a list of associated FlutterEngines (which contains plugins), such that when UISceneDelegate callback is called, we can relay to all the plugins in these engines.

Problem:

There are tons of effort to keep this "associated engines" list up to date. For example:

(1) In FlutterView, we maintain a previousScene state, and when FlutterView moves between UIWindows, we check if it moves between UIScenes:

- (void)willMoveToWindow:(UIWindow*)newWindow {
// When a FlutterView moves windows, it may also be moving scenes. Add/remove the FlutterEngine
// from the FlutterSceneLifeCycleProvider.sceneLifeCycleDelegate if it changes scenes.
UIWindowScene* newScene = newWindow.windowScene;
UIWindowScene* currentScene = self.window.windowScene;
if (newScene == currentScene) {
return;
}
// Remove the engine from the previous scene if it's no longer in that window and scene.
FlutterPluginSceneLifeCycleDelegate* previousSceneLifeCycleDelegate =
[FlutterPluginSceneLifeCycleDelegate fromScene:self.previousScene];
if (previousSceneLifeCycleDelegate) {
[previousSceneLifeCycleDelegate removeFlutterManagedEngine:(FlutterEngine*)self.delegate];
self.previousScene = nil;
}
if (newScene) {
// Add the engine to the new scene's lifecycle delegate.
FlutterPluginSceneLifeCycleDelegate* newSceneLifeCycleDelegate =
[FlutterPluginSceneLifeCycleDelegate fromScene:newScene];
if (newSceneLifeCycleDelegate) {
[newSceneLifeCycleDelegate addFlutterManagedEngine:(FlutterEngine*)self.delegate];
}
} else {
// If the view is being removed from a window, store the current scene to remove the engine
// from it later when the view is added to a new window.
self.previousScene = currentScene;
}
}

(2) This unfortunately doesn't cover willConnectToScene delegate calls, which is called before FlutterView::willMoveToWindow, so we perform a patch when sceneWillConnect is called, to add the engine at the last minute:

if ([scene.delegate conformsToProtocol:@protocol(UIWindowSceneDelegate)]) {
NSObject<UIWindowSceneDelegate>* sceneDelegate =
(NSObject<UIWindowSceneDelegate>*)scene.delegate;
if ([sceneDelegate.window.rootViewController isKindOfClass:[FlutterViewController class]]) {
FlutterViewController* rootViewController =
(FlutterViewController*)sceneDelegate.window.rootViewController;
[self addFlutterManagedEngine:rootViewController.engine];
}
}

(3) We also listen to UISceneWillConnectNotification in FlutterEngine, and patch a missing willConnectScene call if needed for single scene scenario:

- (void)sceneWillConnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
UIScene* scene = notification.object;
if (!FlutterSharedApplication.application.supportsMultipleScenes) {
// Since there is only one scene, we can assume that the FlutterEngine is within this scene and
// register it to the scene.
// The FlutterEngine needs to be registered with the scene when the scene connects in order for
// plugins to receive the `scene:willConnectToSession:options` event.
// If we want to support multi-window on iPad later, we may need to add a way for deveopers to
// register their FlutterEngine to the scene manually during this event.
FlutterPluginSceneLifeCycleDelegate* sceneLifeCycleDelegate =
[FlutterPluginSceneLifeCycleDelegate fromScene:scene];
if (sceneLifeCycleDelegate != nil) {
return [sceneLifeCycleDelegate engine:self receivedConnectNotificationFor:scene];
}
}
}

(inside receivedConnectNotificationFor, it sends the missing sceneWillConnect only if connectionOptions != nil.

(4) However, this doesn't cover all the cases. We also had to provide APIs for developers to manually register/unregister the engine:

/**
* A protocol for manually registering a `FlutterEngine` to receive scene life cycle events.
*/
@protocol FlutterSceneLifeCycleEngineRegistration
/**
* Registers a `FlutterEngine` to receive scene life cycle events.
*
* This method is **only** necessary when the following conditions are true:
* 1. Multiple Scenes (UIApplicationSupportsMultipleScenes) is enabled.
* 2. The `UIWindowSceneDelegate` `window.rootViewController` is not a `FlutterViewController`
* initialized with the target `FlutterEngine`.
*
* When multiple scenes is enabled (UIApplicationSupportsMultipleScenes), Flutter cannot
* automatically associate a `FlutterEngine` with a scene during the scene connection phase. In
* order for plugins to receive launch connection information, the `FlutterEngine` must be manually
* registered with either the `FlutterSceneDelegate` or `FlutterPluginSceneLifeCycleDelegate` during
* `scene:willConnectToSession:options:`.
*
* In all other cases, or once the `FlutterViewController.view` associated with the `FlutterEngine`
* is added to the view hierarchy, Flutter will automatically handle registration for scene events.
*
* Manually registered engines must also be manually deregistered and re-registered if they
* switch scenes. Use `unregisterSceneLifeCycleWithFlutterEngine:`.
*
* @param engine The `FlutterEngine` to register for scene life cycle events.
* @return `NO` if already manually registered.
*/
- (BOOL)registerSceneLifeCycleWithFlutterEngine:(FlutterEngine*)engine;
/**
* Use this method to unregister a `FlutterEngine` from the scene's life cycle events.
*
* @param engine The `FlutterEngine` to unregister for scene life cycle events.
* @return `NO` if the engine was not found among the manually registered engines and could not be
* unregistered.
*/
- (BOOL)unregisterSceneLifeCycleWithFlutterEngine:(FlutterEngine*)engine;
@end

There are still a few issues even with all these efforts:

Proposal

We shouldn't manually maintain the associated engines list, which requires tons of work (we have to maintain), and complicates our public API (developers have to manually register/unregister), and still not 100% correct, as it's only a best effort to keep in sync with the "source of truth".

However, the "source of truth" is already in the UI hierarchy. We can simply perform a DFS search of UIScene to find all the FlutterViewControllers (and hence the vc.engine).

This will also allow us to fully support lifecycle events for multi scenes scenario, because in this simplified world, both single scene and multi-scenes will be just examples of the same model.

This will be a cheap operation, since it's just a walk of in-memory references, and we typically only have a handful of VCs.

An operation with comparable performance is hitTest: when user touch on screen, UIKit performs mulltiple passes of hitTests, each pass is a DFS of the view hierarchy. (what if there are 10,000 views? well, the bottleneck would be rendering these 10,000 views in the first place, and hitTest would be the least of a concern)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listplatform-iosiOS applications specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions