Repository navigation
[ios] Use DFS traversal to find engines of a scene automatically (rather than manually manage the engines list) - #192457
Conversation
| '-destination', | ||
| 'id=$deviceId', | ||
| '-derivedDataPath', | ||
| derivedDataPath, |
There was a problem hiding this comment.
nit: Doesn't matter that much, but probably better location would be in the app's build directory
There was a problem hiding this comment.
updated to derivedDataPath = path.join(appDir.path, 'build', 'DerivedData');
| button.tap() | ||
|
|
||
| let expectedEventsAfterBackgroundAndReactivate = [ | ||
| "sceneWillConnect", |
| super.viewDidLoad() | ||
| view.backgroundColor = .white | ||
|
|
||
| // Create FlutterViewController with engine, but NEVER add it to the view hierarchy or as a child VC. |
There was a problem hiding this comment.
It turns out that we still have to have register/unregister APIs, but we only need it for edge cases when Flutter VC is not added on the screen. This is the test for it.
See #185689
| FLUTTER_DARWIN_EXPORT | ||
| @interface FlutterSceneDelegate | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistration> | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistry> |
There was a problem hiding this comment.
Since no one is using old API, I will just replace it (rather than going the proper deprecate-then-delete route). Renaming the protocol since we added a new scene parameter.
There was a problem hiding this comment.
Ooof, unfortunately I don't think that's true anymore. A quick github search shows people using it: https://github.com/search?q=Flutter+registerSceneLifeCycle+-path%3A%2FHeaders%2FFlutterSceneLifeCycle.h+-path%3A%2FSource%2FFlutterSceneLifeCycle.mm+-path%3A%2FSource%2FFlutterSceneDelegate.mm+-path%3AFlutterSceneDelegateTest.m+-path%3AFlutterSceneLifeCycleTest.mm+-path%3A*.md&type=code
There was a problem hiding this comment.
Since we need to keep the old API, I wonder if it can maintain its current behavior, which is to receive and forwards all events received by the SceneDelegate even though it may be sent to multiple scenes (if the SceneDelegate has multiple scenes).
We can still deprecate it and encourage the new API but I think making it just a no-op is a breaking change.
There was a problem hiding this comment.
I've also been doing some research and I haven't seen any evidence that a SceneDelegate can support multiple scenes. It seems like a 1-to-1 relationship. I tried to create a native app with multiple scenes per delegate and could not figure out a way to do it
There was a problem hiding this comment.
I've also been doing some research and I haven't seen any evidence that a SceneDelegate can support multiple scenes. It seems like a 1-to-1 relationship. I tried to create a native app with multiple scenes per delegate and could not figure out a way to do it
You can create multiple scenes powered by the same scene delegate by calling activateSceneSession multiple times.
Actually the easiest way is just to set the UIScene.delegate property to a shared delegate object: https://developer.apple.com/documentation/uikit/uiscene/delegate
You can change this default delegate object later, as needed.
There was a problem hiding this comment.
Added back the old API and preserved old behavior (also added back the integration test)
| self.sceneLifeCycleDelegate = [[FlutterEnginePluginSceneLifeCycleDelegate alloc] init]; | ||
|
|
||
| if (!FlutterSharedApplication.application.supportsMultipleScenes) { | ||
| [FlutterPluginSceneLifeCycleDelegate registerEngineForSingleScene:self]; |
There was a problem hiding this comment.
This also fixes #191907 (issue with using NSNotification)
| // Tracks whether the application-level sceneWillConnect fallback (which forwards to | ||
| // application:didFinishLaunchingWithOptions:) has already been invoked. This must only be | ||
| // called once per application lifetime. | ||
| static BOOL gSceneWillConnectFallbackCalled = NO; |
There was a problem hiding this comment.
Used to be scene-delegate-scoped, now it's app-scoped
| * This array is lazily cleaned up. `updateFlutterManagedEnginesInScene:` should be called before | ||
| * use to ensure it is up-to-date. | ||
| */ | ||
| @property(nonatomic, strong) NSPointerArray* flutterManagedEngines; |
There was a problem hiding this comment.
We now use DFS rather than manually managing this list
| // Once the connection window ends on the next runloop, entries are removed. | ||
| // This is to ensure engines attached after sceneWillConnect (but still in the same runloop) | ||
| // still receive their sceneWillConnect event (e.g. viewDidLoad of the root view controller). | ||
| @property(nonatomic, strong) NSMapTable<UIScene*, UISceneConnectionOptions*>* connectingScenes; |
There was a problem hiding this comment.
used to be scene-delegate scoped, now it's scene-scoped
| * available. It may not be available if embedded in an iOS app extension or the AppDelegate doesn't | ||
| * subclass FlutterAppDelegate. | ||
| */ | ||
| - (FlutterPluginAppLifeCycleDelegate*)applicationLifeCycleDelegate { |
There was a problem hiding this comment.
this is moved code
There was a problem hiding this comment.
Code Review
This pull request refactors iOS scene lifecycle tracking in Flutter, introducing scene-specific manual engine registration and improving automatic discovery of view controllers in multi-scene applications. The review feedback identifies thread-safety concerns with concurrent access to the global gEnginesForSingleScene array, a potential unintended engine registration when running in environments where UIApplication is unavailable, and potential memory leaks in Swift integration test plugins due to strong static references to plugin instances.
| if (!FlutterSharedApplication.application.supportsMultipleScenes) { | ||
| [FlutterPluginSceneLifeCycleDelegate registerEngineForSingleScene:self]; | ||
| } |
There was a problem hiding this comment.
If FlutterSharedApplication.isAvailable is false (e.g., when running in an app extension), FlutterSharedApplication.application will be nil. In Objective-C, messaging nil returns NO, which means !supportsMultipleScenes will evaluate to YES, and the engine will be unnecessarily registered for the single scene. We should guard this registration with FlutterSharedApplication.isAvailable.
if (FlutterSharedApplication.isAvailable && !FlutterSharedApplication.application.supportsMultipleScenes) {
[FlutterPluginSceneLifeCycleDelegate registerEngineForSingleScene:self];
}
References
- Avoid unnecessary operations and potential nil-messaging side effects by guarding application-specific logic with availability checks.
There was a problem hiding this comment.
Hmmmm, I borrowed this check from the NSNotification call path. Worth thinking about it
| FLUTTER_DARWIN_EXPORT | ||
| @interface FlutterSceneDelegate | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistration> | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistry> |
There was a problem hiding this comment.
Ooof, unfortunately I don't think that's true anymore. A quick github search shows people using it: https://github.com/search?q=Flutter+registerSceneLifeCycle+-path%3A%2FHeaders%2FFlutterSceneLifeCycle.h+-path%3A%2FSource%2FFlutterSceneLifeCycle.mm+-path%3A%2FSource%2FFlutterSceneDelegate.mm+-path%3AFlutterSceneDelegateTest.m+-path%3AFlutterSceneLifeCycleTest.mm+-path%3A*.md&type=code
| [self searchFlutterViewControllersWithResult:result | ||
| visited:visited | ||
| viewController:viewController.presentedViewController]; | ||
| } |
There was a problem hiding this comment.
Do you think there's any concern in Apple introducing new APIs that this doesn't account for?
For example, UIArrangementViewController was added in 27.1 beta. One would guess that the "container view controllers" will be found via childViewControllers, but what if they're not and need to accessed differently such as with the presentedViewController?
There was a problem hiding this comment.
If apple introduces a new relationship between VCs, then yes, we have to update this code. However, this hasn't changed for like 15+ years.
The UIArrangementViewController should still use child/parent relationship since it's a container (just like a UINavigationController).
| // Close the connection window on the next run loop turn. | ||
| __weak __typeof(self) weakSelf = self; | ||
| __weak __typeof(scene) weakScene = scene; | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| __typeof(self) strongSelf = weakSelf; | ||
| __typeof(scene) strongScene = weakScene; | ||
| if (strongSelf && strongScene) { | ||
| [strongSelf.connectingScenes removeObjectForKey:strongScene]; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Can you explain why this is needed?
There was a problem hiding this comment.
Updated with more comments.
| FLUTTER_DARWIN_EXPORT | ||
| @interface FlutterSceneDelegate | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistration> | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistry> |
There was a problem hiding this comment.
Since we need to keep the old API, I wonder if it can maintain its current behavior, which is to receive and forwards all events received by the SceneDelegate even though it may be sent to multiple scenes (if the SceneDelegate has multiple scenes).
We can still deprecate it and encourage the new API but I think making it just a no-op is a breaking change.
| FLUTTER_DARWIN_EXPORT | ||
| @interface FlutterSceneDelegate | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistration> | ||
| : NSObject <UIWindowSceneDelegate, FlutterSceneLifeCycleEngineRegistry> |
There was a problem hiding this comment.
I've also been doing some research and I haven't seen any evidence that a SceneDelegate can support multiple scenes. It seems like a 1-to-1 relationship. I tried to create a native app with multiple scenes per delegate and could not figure out a way to do it
| // Tracks all engines for single-scene applications. | ||
| // We have to use a global variable because the FlutterEngine could be created before | ||
| // UISceneDelegate object is created. | ||
| static NSPointerArray* gEnginesForSingleScene; |
There was a problem hiding this comment.
Perhaps consider using a singleton?
There was a problem hiding this comment.
Technically speaking this is a singleton.
However, I do notice that in other files we put these values scoped under a class, so I updated to follow existing convention.
| } | ||
|
|
||
| [self compactNSPointerArray:self.developerManagedEngines]; | ||
| - (NSArray<FlutterViewController*>*)searchFlutterViewControllersWithScene:(UIScene*)scene { |
There was a problem hiding this comment.
Nit:
| - (NSArray<FlutterViewController*>*)searchFlutterViewControllersWithScene:(UIScene*)scene { | |
| - (NSArray<FlutterViewController*>*)searchFlutterViewControllersWithinScene:(UIScene*)scene { |
| } | ||
|
|
||
| engine.manuallyRegisteredToScene = YES; | ||
| - (NSArray<FlutterEngine*>*)searchFlutterEnginesWithScene:(UIScene*)scene { |
There was a problem hiding this comment.
Nit:
| - (NSArray<FlutterEngine*>*)searchFlutterEnginesWithScene:(UIScene*)scene { | |
| - (NSArray<FlutterEngine*>*)searchFlutterEnginesWithinScene:(UIScene*)scene { |
| // called once per application lifetime. | ||
| static BOOL gSceneWillConnectFallbackCalled = NO; | ||
|
|
||
| static BOOL IsPowerOfTwo(NSUInteger x) { |
There was a problem hiding this comment.
Can you add a comment on what this does and why it's required?
| * This method is **only** necessary in multi-scene applications when an engine needs to receive | ||
| * a scene's life-cycle events before its `FlutterViewController` is added to that scene (for | ||
| * example, pre-warmed engines in add-to-app workflows), or when the engine has no view controller | ||
| * at all (headless engines). |
There was a problem hiding this comment.
Nit: I personally like the format where it's clearly outlined when this should be used
This method is **only** necessary when the following conditions are true:
* 1. x
* 2. y
* 3. z
update gSceneWillConnectFallbackCalled to global
f2f2425 to
fb339dd
Compare
fb339dd to
19dd616
Compare
| * created, so manual registration is never required. | ||
| */ | ||
| @protocol FlutterSceneLifeCycleEngineRegistration | ||
| @protocol FlutterSceneLifeCycleEngineRegistry |
There was a problem hiding this comment.
Does this need to be a new protocol? Can we just keep the old protocol and make the methods optional? What's the pros and cons?
There was a problem hiding this comment.
Making these methods optional would be wrong semantics - they are both mandatory for a functional registry. Leaving them optional also introduces confusing edge cases, such as an implementation providing register while omitting unregister.
flutter/flutter@5c7e45a...a3d71d8 2026-10-16 [email protected] 0 Initialize variables in the DlComplexityPathReceiver helper (flutter/flutter#194540) 2026-10-16 [email protected] Roll Fuchsia Test Scripts from V-qQL4158Nw6L-QLi... to 2T18KC7-LDQtmQiLP... (flutter/flutter#194527) 2026-10-16 [email protected] Remove BorderMaskBlur (flutter/flutter#194508) 2026-10-16 [email protected] Roll Skia from a577264dc087 to 470b8816a261 (1 revision) (flutter/flutter#194526) 2026-10-16 [email protected] [ios] Use DFS traversal to find engines of a scene automatically (rather than manually manage the engines list) (flutter/flutter#192457) 2026-10-16 [email protected] [Skia][DisplayList] Map local bounds through the local matrix in DlLocalMatrixImageFilter (flutter/flutter#193963) 2026-10-16 [email protected] [framework] Fix avoid_setters_without_getters warnings on mixin declarations (flutter/flutter#193856) 2026-10-16 [email protected] Roll Dart SDK from 84686ea04815 to 55a734a01fb1 (3 revisions) (flutter/flutter#194522) 2026-10-16 [email protected] Roll Skia from d131b7789482 to a577264dc087 (7 revisions) (flutter/flutter#194514) 2026-10-16 [email protected] Roll Fuchsia GN SDK from PDJz3b3CL31uoLOkD... to kFdyRnojbVy0cxvRZ... (flutter/flutter#194510) 2026-10-16 [email protected] Roll GN to f9eca75ae554 (flutter/flutter#194471) 2026-10-16 [email protected] refactor(flutter_tools): migrate RunCommand and RunCommandBase to OptionDescriptor and OptionBundle (flutter/flutter#194362) 2026-10-15 [email protected] refactor(flutter_tools): finish TestCommand OptionDescriptor and OptionBundle migration (flutter/flutter#194361) 2026-10-15 [email protected] [iOS][a11y] Fix VoiceOver announcement for unfocused password fields (flutter/flutter#192851) 2026-10-15 [email protected] Roll pub packages (flutter/flutter#194477) 2026-10-15 [email protected] refactor(flutter_tools): finish DriveCommand OptionDescriptor migration (flutter/flutter#194360) 2026-10-15 [email protected] [flutter_tools] Roll package:vm_service to 15.4.0 (flutter/flutter#194467) 2026-10-15 [email protected] refactor(flutter_tools): migrate AttachCommand to OptionDescriptor and OptionBundle (flutter/flutter#194359) 2026-10-15 [email protected] [flutter_tools] Inject dependencies into update-packages and package_map (flutter/flutter#194046) 2026-10-15 [email protected] [impeller] parallelize opengles shader compiles (flutter/flutter#194022) 2026-10-15 [email protected] Roll pub packages (flutter/flutter#194469) 2026-10-15 [email protected] Restrict web dev server to loopback by default (flutter/flutter#187595) 2026-10-15 [email protected] Roll Packages from a1ed23c to da40eaf (33 revisions) (flutter/flutter#194459) 2026-10-15 [email protected] [web] Render soft hyphens on Skwasm (flutter/flutter#193850) 2026-10-15 [email protected] refactor(flutter_tools): migrate AssembleCommand to OptionDescriptor (flutter/flutter#194358) 2026-10-15 [email protected] [windows] Add OpenGL golden test against ANGLE at D3D11 feature level 10_0 (flutter/flutter#194321) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…her than manually manage the engines list) (flutter#192457) This PR simplifies iOS embedder's UIScene lifecycle logic. The key idea is to perform DFS traversal to find engines to send scene events to (rather than manually manage the `flutterManagedEngines` list). By doing so, we are able to remove the need to manual registering engines (mostly, see below) and other logics too. This PR also fixes a few bugs along the way for both single and multi-scenes. ### Simplifications: [multi-scene] Use DFS to find engines of a scene automatically (rather than manually manage `flutterManagedEngines` list) [multi-scene] Manual register/unregister is only required for cases where FlutterVC is not added to the scene but still wanna receive events (e.g. headless/add-to-app) (Fixes flutter#185689) [single-scene] Remove NSNotification code path for connection events ### Bug fixes [single-scene] Fixes missing connection event for an edge case of implicit engine usage (Fixes flutter#186547) [single-scene] Fix missing lifecycle events if the engine is not attached to any scenes (e.g. headless engines/add-to-app) (Fixes flutter#191907) [multi-scene] connectionOptions should be per scene (Fixes flutter#191039) [multi-scene] Manual register/unregister API is now registered per scene rather than per scene delegate (added `scene` parameter) [multi-scene] Scene lifecycle listeners are not per-scene rather than per scene delegate (Fixes flutter#185687) Also fixes flutter#185688 Also fixes flutter#185702 Also fixes flutter#192344 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [ ] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This PR simplifies iOS embedder's UIScene lifecycle logic. The key idea is to perform DFS traversal to find engines to send scene events to (rather than manually manage the
flutterManagedEngineslist).By doing so, we are able to remove the need to manual registering engines (mostly, see below) and other logics too.
This PR also fixes a few bugs along the way for both single and multi-scenes.
Simplifications:
[multi-scene] Use DFS to find engines of a scene automatically (rather than manually manage
flutterManagedEngineslist)[multi-scene] Manual register/unregister is only required for cases where FlutterVC is not added to the scene but still wanna receive events (e.g. headless/add-to-app) (Fixes #185689)
[single-scene] Remove NSNotification code path for connection events
Bug fixes
[single-scene] Fixes missing connection event for an edge case of implicit engine usage (Fixes #186547)
[single-scene] Fix missing lifecycle events if the engine is not attached to any scenes (e.g. headless engines/add-to-app) (Fixes #191907)
[multi-scene] connectionOptions should be per scene (Fixes #191039)
[multi-scene] Manual register/unregister API is now registered per scene rather than per scene delegate (added
sceneparameter)[multi-scene] Scene lifecycle listeners are not per-scene rather than per scene delegate (Fixes #185687)
Also fixes #185688
Also fixes #185702
Also fixes #192344
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.