Skip to content

feat(ios): Add UIScene lifecycle support for background app refresh#1

Merged
ppamorim merged 1 commit intomasterfrom
UIScene-life-cycle-support
Apr 17, 2026
Merged

feat(ios): Add UIScene lifecycle support for background app refresh#1
ppamorim merged 1 commit intomasterfrom
UIScene-life-cycle-support

Conversation

@ppamorim
Copy link
Copy Markdown

Summary

  • Adds UIScene lifecycle support so background app refresh is scheduled correctly in apps that adopt UISceneDelegate (where applicationDidEnterBackground is never called).
  • Introduces SceneLifecycleBridge, a small abstraction that registers itself with Flutter's addSceneDelegate via an ObjC runtime check (responds(to:) / perform(_:with:)), so the plugin compiles on all Flutter versions ≥ 3.22.0 — not only those that ship addSceneDelegate (introduced in Flutter 3.38.0).
  • Adds FlutterForegroundTaskEarlyRegistration (+load) in the ObjC layer to register the BGTaskScheduler handler at binary load time, before Flutter initialises — required when iOS wakes the app in the background to service a refresh task.
  • Adds an isBgTaskRegistered guard to prevent a double-registration crash during the UIScene transition period.

Based on Dev-hwang/flutter_foreground_task#378 by @Gibbo97, adapted to avoid bumping the minimum Flutter version.

Files changed

File What
ios/Classes/SceneLifecycleBridge.swift New. Bridges UIScene lifecycle → caller-supplied closure via ObjC runtime check.
ios/Classes/SwiftFlutterForegroundTaskPlugin.swift Registers the bridge, adds registerAppRefreshForBackgroundLaunch(), adds isBgTaskRegistered guard.
ios/Classes/FlutterForegroundTaskPlugin.m Adds FlutterForegroundTaskEarlyRegistration +load to register BGTask handler before Flutter init.

Key design decision

The original PR (Dev-hwang#378) bumps flutter: ">=3.38.0" and declares FlutterSceneLifeCycleDelegate conformance. This PR avoids both:

  1. No protocol conformance in the type declaration — Flutter dispatches scene lifecycle calls via respondsToSelector:, so marking sceneDidEnterBackground as @objc is sufficient.
  2. addSceneDelegate called via NSSelectorFromString + responds(to:) — silently no-ops on older Flutter, works normally on ≥ 3.38.0.

This keeps the minimum Flutter version at >=3.22.0.

Test plan

  • Build with Flutter ≥ 3.38.0, verify sceneDidEnterBackground fires and schedules background refresh
  • Build with Flutter < 3.38.0, verify plugin compiles and the scene bridge silently no-ops
  • Verify background-launch scenario (iOS wakes app for BGAppRefreshTask) works via the +load early registration
  • Verify no double-registration crash when GeneratedPluginRegistrant.register(with:) is called from multiple locations

Apple requires UIKit apps built with the latest SDK to adopt the
UIScene lifecycle. In UIScene apps applicationDidEnterBackground is
never called, which prevented background app refresh from being
scheduled.

Changes:
- Add SceneLifecycleBridge that registers itself via an ObjC runtime
  check (responds(to:) / perform(_:with:)), so the plugin compiles on
  all Flutter versions >= 3.22.0, not only those that expose
  addSceneDelegate (introduced in Flutter 3.38.0).
- sceneDidEnterBackground schedules background app refresh, mirroring
  what applicationDidEnterBackground does for non-UIScene apps.
- Add FlutterForegroundTaskEarlyRegistration +load in the ObjC layer
  to register the BGTaskScheduler handler at binary load time, before
  Flutter initialises — required for background-launch scenarios.
- Add isBgTaskRegistered guard to registerAppRefresh() to prevent a
  double-registration crash during the UIScene transition period.

Based on Dev-hwang#378 by @Gibbo97, adapted to
avoid bumping the minimum Flutter version.
@ppamorim ppamorim force-pushed the UIScene-life-cycle-support branch from 638f188 to b9dbad3 Compare April 17, 2026 12:13
@sonarqubecloud
Copy link
Copy Markdown

@ppamorim ppamorim merged commit 83c5b36 into master Apr 17, 2026
4 checks passed
@ppamorim ppamorim deleted the UIScene-life-cycle-support branch April 17, 2026 19:45
ppamorim added a commit that referenced this pull request Apr 18, 2026
The MethodChannel-based debugThreadId() always measured the platform
main thread (every FlutterEngine shares the same platform task runner on
Android/iOS), making it impossible to distinguish mergedEngine from
dedicatedEngine. This replaces the native handlers with an in-isolate
probe via package:universal_ffi/ffi.dart:

- Android: gettid() from libc.so
- iOS/macOS: pthread_mach_thread_np(pthread_self()) via DynamicLibrary.process()
- Other platforms: null

The probe initialization is async (universal_ffi requires it on Android)
and cached after first call. Platform detection is moved to
package:platform/platform.dart (already a direct dependency) instead of
dart:io. Removed universal_io; added universal_ffi to pubspec.yaml.

Stale native debugThreadId handlers removed from:
  - MethodCallHandlerImpl.kt
  - service/ForegroundTask.kt
  - SwiftFlutterForegroundTaskPlugin.swift
  - service/ForegroundTask.swift

Documentation updated:
  - merged_platform_ui_thread_mitigation.md: new §2 "Known bugs" section
    listing this as Bug #1 with root cause, symptom, and fix plan; all
    downstream sections renumbered; risk table row marked resolved.
  - threading_model.md: runtime verification section updated from
    "proposed" to "implemented"; old MethodChannel history preserved.
  - CHANGELOG.md: debugThreadId entry revised to reflect FFI semantics.

Tests updated: debugThreadId group now asserts the helper never hits
either MethodChannel, returns a positive tid on supported hosts, and
is stable across calls on the same isolate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant