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)
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
previousScenestate, and when FlutterView moves betweenUIWindows, we check if it moves betweenUIScenes:flutter/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm
Lines 317 to 347 in 828b6dc
(2) This unfortunately doesn't cover
willConnectToScenedelegate calls, which is called beforeFlutterView::willMoveToWindow, so we perform a patch whensceneWillConnectis called, to add the engine at the last minute:flutter/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm
Lines 195 to 203 in 828b6dc
(3) We also listen to
UISceneWillConnectNotificationin FlutterEngine, and patch a missingwillConnectScenecall if needed for single scene scenario:flutter/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Lines 317 to 332 in a51da69
(inside
receivedConnectNotificationFor, it sends the missingsceneWillConnectonly ifconnectionOptions != nil.(4) However, this doesn't cover all the cases. We also had to provide APIs for developers to manually register/unregister the engine:
flutter/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterSceneLifeCycle.h
Lines 101 to 138 in 828b6dc
There are still a few issues even with all these efforts:
sceneWillConnectregistration is missing corner cases #185688Proposal
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 thevc.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, andhitTestwould be the least of a concern)