Summary
On iOS, when the app has been in the background for a long time (e.g. 30+ minutes, or after prolonged music playback in another app), returning to the app often results in a white or black screen with no Flutter UI. The process is running and the system reports the app as visible and active, but nothing is rendered. Force-quitting and relaunching usually fixes it until the next long background.
Environment
- Flutter: 3.38.9 (stable)
- Platform: iOS (physical device; issue is rare or absent on simulator)
- Architecture: Standard Flutter app with UIScene lifecycle (SceneDelegate), single window
- Rendering: Impeller (default on iOS since 3.29; Skia not available)
- Reproduction: More likely after long background and/or when another app (e.g. music) has been in foreground
Steps to reproduce
- Launch the Flutter app on a physical iOS device.
- Send the app to background (home or app switcher).
- Leave the device with the app in background for a long time (e.g. 30+ minutes), optionally with another app (e.g. music) in foreground.
- Bring the Flutter app back to foreground (tap icon or from app switcher).
Result: Screen stays white (or black). No Flutter content is drawn. App does not crash; it just fails to render.
Expected: App should show the last screen or the appropriate UI after resume.
Observations from system logs
- SpringBoard / runningboard report the app as running-active, Visible, and UserInteractiveFocal.
- Scene lifecycle reaches Foreground and scene content state becomes ready.
- Runner (the app process) sometimes reports itself as unknown-NotVisible or running-active-NotVisible in RBS/state updates while the system considers the app visible—suggesting an internal visibility or rendering-pipeline mismatch.
- Logs often contain:
Returning cached initialization context (warm/resume path) and sometimes updated settings were not derived from the scene's previous settings.
- No Flutter or engine crash; the failure is “no frame painted” rather than a crash.
Suspected root cause (for implementers)
We believe this is related to Impeller/Metal surface lifecycle after long background:
- Surface invalidation: After long background, iOS may reclaim or invalidate GPU/Metal resources (CAMetalLayer, drawables, or context). The Flutter engine’s Metal surface may no longer be valid.
- No automatic surface recreation: Engine creates the surface in
viewDidLayoutSubviews when the app is active. If the view’s bounds do not change on resume, layout may not run again, so the engine never recreates the surface and keeps using an invalid one → nothing is drawn.
- Frame vs. surface timing: When the app becomes active, Dart receives
resumed and typically calls scheduleFrame() once. If the Metal surface is recreated asynchronously and the first frame is requested before the new surface is ready, that frame can be dropped and no further frame is scheduled → prolonged white/black screen.
So the issue likely combines: (a) surface invalidation after long background, (b) no bounds change so no viewDidLayoutSubviews-driven surface recreation, and (c) first frame scheduled too early relative to surface readiness.
Workarounds we use (app-side)
We cannot fix the engine from the app; the following only mitigate:
- Force surface invalidation (SceneDelegate): On
sceneWillEnterForeground and sceneDidBecomeActive, we temporarily change the Flutter view’s frame (e.g. insetBy(dx: 0, dy: 0.5) then restore) so the engine sees a bounds change and recreates the surface in viewDidLayoutSubviews. We repeat this at 0.3s, 0.6s, 1s, 2s, 3s to cover slow GPU recovery.
- Multiple delayed
scheduleFrame() (Dart): In WidgetsBindingObserver.didChangeAppLifecycleState(resumed), we call WidgetsBinding.instance.scheduleFrame() immediately and again at 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000 ms so that at least one frame request happens after the new surface is ready.
runApp first: We avoid blocking runApp() on async init (e.g. Firebase, Prefs) so the first frame is scheduled as soon as possible; heavy init runs after the first frame.
FLTDisablePartialRepaint (Info.plist): We set this to YES as a possible mitigation for partial-repaint-related glitches; it may or may not help this specific issue.
These workarounds reduce but do not fully eliminate the problem, especially after very long background (e.g. hours) or under memory pressure.
What we are asking for
- Engine / framework: Ensure that when the app returns to foreground after long background, the Impeller/Metal surface is recreated or revalidated (e.g. in response to scene activation or a guaranteed layout pass), and that at least one frame is scheduled after the new surface is ready, so that Flutter UI is drawn without app-side hacks.
- Documentation: If there are recommended patterns or constraints for UIScene + Impeller on iOS (e.g. when to trigger layout, or how to avoid this scenario), documenting them would help.
Related issues / PRs (if any)
Checklist
Summary
On iOS, when the app has been in the background for a long time (e.g. 30+ minutes, or after prolonged music playback in another app), returning to the app often results in a white or black screen with no Flutter UI. The process is running and the system reports the app as visible and active, but nothing is rendered. Force-quitting and relaunching usually fixes it until the next long background.
Environment
Steps to reproduce
Result: Screen stays white (or black). No Flutter content is drawn. App does not crash; it just fails to render.
Expected: App should show the last screen or the appropriate UI after resume.
Observations from system logs
Returning cached initialization context(warm/resume path) and sometimesupdated settings were not derived from the scene's previous settings.Suspected root cause (for implementers)
We believe this is related to Impeller/Metal surface lifecycle after long background:
viewDidLayoutSubviewswhen the app is active. If the view’s bounds do not change on resume, layout may not run again, so the engine never recreates the surface and keeps using an invalid one → nothing is drawn.resumedand typically callsscheduleFrame()once. If the Metal surface is recreated asynchronously and the first frame is requested before the new surface is ready, that frame can be dropped and no further frame is scheduled → prolonged white/black screen.So the issue likely combines: (a) surface invalidation after long background, (b) no bounds change so no
viewDidLayoutSubviews-driven surface recreation, and (c) first frame scheduled too early relative to surface readiness.Workarounds we use (app-side)
We cannot fix the engine from the app; the following only mitigate:
sceneWillEnterForegroundandsceneDidBecomeActive, we temporarily change the Flutter view’s frame (e.g.insetBy(dx: 0, dy: 0.5)then restore) so the engine sees a bounds change and recreates the surface inviewDidLayoutSubviews. We repeat this at 0.3s, 0.6s, 1s, 2s, 3s to cover slow GPU recovery.scheduleFrame()(Dart): InWidgetsBindingObserver.didChangeAppLifecycleState(resumed), we callWidgetsBinding.instance.scheduleFrame()immediately and again at 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000 ms so that at least one frame request happens after the new surface is ready.runAppfirst: We avoid blockingrunApp()on async init (e.g. Firebase, Prefs) so the first frame is scheduled as soon as possible; heavy init runs after the first frame.FLTDisablePartialRepaint(Info.plist): We set this toYESas a possible mitigation for partial-repaint-related glitches; it may or may not help this specific issue.These workarounds reduce but do not fully eliminate the problem, especially after very long background (e.g. hours) or under memory pressure.
What we are asking for
Related issues / PRs (if any)
viewDidLayoutSubviewsonly when app is active.Checklist