Skip to content

[ECO-5694] Adopt UIScene life cycle for iOS push notifications - #603

Draft
ttypic wants to merge 7 commits into
mainfrom
ECO-5694/ui-scene-refactoring
Draft

[ECO-5694] Adopt UIScene life cycle for iOS push notifications#603
ttypic wants to merge 7 commits into
mainfrom
ECO-5694/ui-scene-refactoring

Conversation

@ttypic

@ttypic ttypic commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

introduces support for the UIScene architecture, which is required by iOS SDK builds targeting iOS 26 and later:

  • Plugin updates for UIScene support:

    • The plugin now conforms to FlutterSceneLifeCycleDelegate to handle UISceneDelegate events alongside UIApplicationDelegate ones.
    • Handles the transition to pull notification payloads from both UIApplicationLaunchOptionsRemoteNotificationKey and UIScene’s connection options.
  • Example and integration-test updates:

    • The reference apps now adopt UIScene by including a UIApplicationSceneManifest and using the recommended FlutterSceneDelegate.
  • Push notification integration adjustments:

    • Provides an API for apps to register Ably’s push notification handlers directly via AblyFlutter.sharedInstance().registerPushNotificationHandlers() before Flutter plugin registration.
    • Fixes existing bugs in push notifications logic, including proper handling of edge cases for pushNotificationTapLaunchedAppFromTerminated.
  • Compatibility bumps:

    • Raises the minimum supported versions to Flutter 3.38 and iOS 13.0 to utilize UIScene APIs.
  • Additional improvements:

    • Reformats codebase using Dart 3.11's formatter for consistency.
    • Updates documentation to match these changes in PushNotifications.md and UPDATING.md.

ttypic and others added 7 commits August 3, 2026 20:35
The UIScene plugin APIs this SDK is about to adopt — the
`FlutterSceneLifeCycleDelegate` protocol and
`FlutterPluginRegistrar.addSceneDelegate:` — first shipped in Flutter 3.38,
and `UIScene` itself is iOS 13+. Raise both floors so the iOS migration in
the following commits can compile, and pull the toolchain and CI up to
Flutter 3.41.9 to match.

Alongside the version bumps:

- Drop the removed `package_api_docs` lint and the two now-redundant `!`
  assertions in `codec.dart` that the newer analyzer flags, so
  `flutter analyze` is clean again.
- Silence `experimental_member_use` on the example app's deliberate use of
  the experimental `Push.reset`.
- Gitignore `ios/Flutter/ephemeral/`, which Flutter 3.41 generates in both
  example projects.
- Regenerating the CocoaPods projects also drops stale Firebase, Toast and
  nanopb framework entries from the example's `project.pbxproj`; those pods
  are no longer in its Podfile.

Groundwork for #592 and #602.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`dart format --set-exit-if-changed .` is a CI gate, and the formatter's
output changed with the SDK bump in the previous commit, so the whole tree
has to be reformatted. Kept as its own commit because it touches 134 files
and none of it is behavioural — review it separately from, or skip it
alongside, the UIScene work that follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hLaunching

Apple requires `UNUserNotificationCenter.delegate` to be set before
`application:didFinishLaunchingWithOptions:` returns, but for apps on the
UIScene life cycle Flutter defers plugin registration until after it returns.
The plugin installed the delegate during registration, which is too late — so
expose a shared instance and an explicit API that apps call themselves, as the
Flutter team asked in #602:

    AblyFlutter.sharedInstance().registerPushNotificationHandlers()

`AblyFlutter` becomes a process-wide singleton to make that reachable before
Flutter has registered anything. Its remaining state was already process-wide
(`AblyInstanceStore` and `PushActivationEventHandlers` are both singletons),
and registration now configures the shared instance rather than allocating a
new one.

Plugin registration still installs the delegate itself, so apps on the
`UIApplicationDelegate` life cycle need no source changes. Installing is now
idempotent — previously each call wrapped whatever delegate was present, so
being invoked twice would have delivered every notification event to Dart once
per wrapper.

Because the delegate can now be installed before any Flutter engine exists,
`PushNotificationEventHandlers` takes its method channel separately from
construction and states what each handler does while there is no channel. The
one case that must not be lost is a tap that launched the app: its payload
goes to the property Dart already polls on startup.

Two bugs on that same path, which the UIScene work depends on:

- `pushNotificationTapLaunchedAppFromTerminated` returned the raw APNs
  payload, which the codec cannot encode as the `RemoteMessage` Dart expects,
  and never delivered a result at all when the app was not launched by a tap —
  leaving Dart awaiting forever.
- `application:didReceiveRemoteNotification:fetchCompletionHandler:` silently
  dropped the notification when `AblyFlutterHandlePushNotifications` is `NO`,
  never calling the completion handler. It now declines the event so another
  application delegate can handle it.

Also drops `UNUserNotificationCenterDelegate` from `AblyFlutter`'s declared
conformances: it implements none of those methods (`PushNotificationEventHandlers`
does), so the conformance only invited callers to install the wrong object.

Resolves #602.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Apple will require UIKit apps built against the SDK after iOS 26 to use the
UIScene life cycle, and Flutter is following, so the plugin has to handle
`UISceneDelegate` events as well as `UIApplicationDelegate` ones.

`AblyFlutter` now conforms to `FlutterSceneLifeCycleDelegate` and registers via
`addSceneDelegate:`, implementing `scene:willConnectToSession:options:` — the
scene equivalent of `application:didFinishLaunchingWithOptions:`. It stays
registered as an application delegate too, so apps on either life cycle work.

The reason this event is needed rather than optional: Flutter does forward
`application:didFinishLaunchingWithOptions:` to plugins in a scene app, but
with nil launch options. So the check for a notification tap having launched the
app, which read `UIApplicationLaunchOptionsRemoteNotificationKey`, would
silently stop finding anything. The payload now comes from the scene's
`connectionOptions.notificationResponse` instead, and whichever of the two
callbacks runs first wins.

Nothing else in the plugin needed migrating: the only `UIApplication` use is
`registerForRemoteNotifications`, there are no observers on `UIApplication`
notifications, and none of the deprecated `keyWindow` / `UIScreen.mainScreen` /
`UIApplication.windows` accessors are used. Foreground and background state is
read from `UIApplication.applicationState` on demand, which still works under
UIScene.

Resolves #592.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ne life cycle

Neither app opted into UIScene, so nothing exercised the scene code paths the
previous two commits added. Both now declare a `UIApplicationSceneManifest` and
call `registerPushNotificationHandlers` from their `AppDelegate`, which is also
the reference the docs point at.

The manifest names Flutter's own `FlutterSceneDelegate` as the scene delegate
class rather than introducing an app-level subclass, since neither app needs to
customise it — its header documents this as the intended use. `UIMainStoryboardFile`
goes away because `UISceneStoryboardFile` supersedes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pod lib lint` resolves `s.dependency 'Flutter'` from CocoaPods trunk, where the
`Flutter` pod is pinned at 3.13.0. That predates the UIScene plugin APIs, so the
iOS unit test job started failing with "cannot find protocol declaration for
'FlutterSceneLifeCycleDelegate'".

The `Flutter.podspec` that Flutter tooling generates inside an app project is no
help either — it is a placeholder with no headers, because real builds link the
framework through xcconfig rather than CocoaPods. So supply a development-only
podspec that vendors the engine artifacts of whichever Flutter SDK is on PATH,
and pass it to lint with `--include-podspecs`.

It symlinks the framework in beside itself because CocoaPods rejects absolute
file patterns and silently ignores relative ones that escape the pod root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a UIScene section to PushNotifications.md covering the
`registerPushNotificationHandlers` call scene-based apps must make, in Swift and
Objective-C, and why Apple's ordering requirement makes it necessary. Adds a
migration entry to UPDATING.md for that call and for the Flutter 3.38 / iOS 13
minimums.

Also documents the `AblyFlutterHandlePushNotifications` Info.plist opt-out,
which has been undocumented since it was added, and fixes references to an
example `AppDelegate.m` that has been Swift for some time. The claim that
foreground presentation is decided in the app's AppDelegate was wrong too — the
plugin owns `willPresentNotification` and asks Dart.

CHANGELOG entries are left to the release process, which generates them from
merged PRs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 169 files, which is 69 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c095d84-0c41-459c-bec2-cd78c7ef04da

📥 Commits

Reviewing files that changed from the base of the PR and between 10fc778 and e3b19ba.

⛔ Files ignored due to path filters (5)
  • example/ios/Podfile.lock is excluded by !**/*.lock
  • example/pubspec.lock is excluded by !**/*.lock
  • pubspec.lock is excluded by !**/*.lock
  • test_integration/ios/Podfile.lock is excluded by !**/*.lock
  • test_integration/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (169)
  • .github/workflows/check.yaml
  • .github/workflows/docs.yml
  • .github/workflows/flutter_example_app.yaml
  • .github/workflows/flutter_integration.yaml
  • .github/workflows/ios_unit_tests.yml
  • .pubignore
  • .tool-versions
  • PushNotifications.md
  • README.md
  • UPDATING.md
  • analysis_options.yaml
  • bin/codegen_context.dart
  • bin/templates/platformconstants.dart.dart
  • bin/templates/platformconstants.h.dart
  • bin/templates/platformconstants.java.dart
  • bin/templates/platformconstants.m.dart
  • example/ios/.gitignore
  • example/ios/Flutter/AppFrameworkInfo.plist
  • example/ios/Podfile
  • example/ios/Runner.xcodeproj/project.pbxproj
  • example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  • example/ios/Runner/AppDelegate.swift
  • example/ios/Runner/Info.plist
  • example/lib/app_provisioning.dart
  • example/lib/main.dart
  • example/lib/nested_realtime_events.dart
  • example/lib/op_state.dart
  • example/lib/push_notifications/android_push_notification_configuration.dart
  • example/lib/push_notifications/push_notification_handlers.dart
  • example/lib/push_notifications/push_notification_message_examples.dart
  • example/lib/push_notifications/push_notification_service.dart
  • example/lib/ui/ably_service.dart
  • example/lib/ui/api_key_service.dart
  • example/lib/ui/auth.dart
  • example/lib/ui/bool_stream_button.dart
  • example/lib/ui/paginated_result_viewer.dart
  • example/lib/ui/push_notifications/push_notifications_activation_sliver.dart
  • example/lib/ui/push_notifications/push_notifications_device_information.dart
  • example/lib/ui/push_notifications/push_notifications_ios_permissions_sliver.dart
  • example/lib/ui/push_notifications/push_notifications_publishing_sliver.dart
  • example/lib/ui/push_notifications/push_notifications_received_sliver.dart
  • example/lib/ui/push_notifications/push_notifications_sliver.dart
  • example/lib/ui/push_notifications/push_notifications_subscriptions_sliver.dart
  • example/lib/ui/push_notifications/push_realtime_client_received_sliver.dart
  • example/lib/ui/realtime_presence_sliver.dart
  • example/lib/ui/realtime_sliver.dart
  • example/lib/ui/rest_sliver.dart
  • example/lib/ui/system_details_sliver.dart
  • example/lib/ui/text_row.dart
  • example/lib/ui/utilities.dart
  • example/pubspec.yaml
  • ios/.gitignore
  • ios/Classes/AblyFlutter.h
  • ios/Classes/AblyFlutter.m
  • ios/Classes/PushNotificationEventHandlers.swift
  • ios/Classes/handlers/PushHandlers.swift
  • ios/Tests/Support/Flutter.podspec
  • ios/ably_flutter.podspec
  • lib/src/authentication/src/auth_options.dart
  • lib/src/authentication/src/client_options.dart
  • lib/src/authentication/src/token_details.dart
  • lib/src/authentication/src/token_request.dart
  • lib/src/crypto/src/crypto.dart
  • lib/src/error/src/ably_exception.dart
  • lib/src/error/src/error_info.dart
  • lib/src/logging/src/log_handler.dart
  • lib/src/message/src/delta_extras.dart
  • lib/src/message/src/message.dart
  • lib/src/message/src/message_data.dart
  • lib/src/message/src/message_extras.dart
  • lib/src/message/src/presence_message.dart
  • lib/src/platform/src/ably_event_message.dart
  • lib/src/platform/src/ably_message.dart
  • lib/src/platform/src/background_android_isolate_platform.dart
  • lib/src/platform/src/cipher_params_internal.dart
  • lib/src/platform/src/codec.dart
  • lib/src/platform/src/method_call_handler.dart
  • lib/src/platform/src/paginated_result.dart
  • lib/src/platform/src/platform.dart
  • lib/src/platform/src/platform_object.dart
  • lib/src/platform/src/push_notification_events_internal.dart
  • lib/src/platform/src/realtime/connection.dart
  • lib/src/platform/src/realtime/presence.dart
  • lib/src/platform/src/realtime/realtime.dart
  • lib/src/platform/src/realtime/realtime_channel.dart
  • lib/src/platform/src/rest/rest_channel.dart
  • lib/src/platform/src/rest/rest_presence.dart
  • lib/src/platform/src/streams_channel.dart
  • lib/src/push_notifications/src/admin/push_device_registrations.dart
  • lib/src/push_notifications/src/device_push_details.dart
  • lib/src/push_notifications/src/ios_notification_settings.dart
  • lib/src/push_notifications/src/local_device.dart
  • lib/src/push_notifications/src/notification.dart
  • lib/src/push_notifications/src/push.dart
  • lib/src/push_notifications/src/push_channel.dart
  • lib/src/push_notifications/src/push_channel_subscriptions.dart
  • lib/src/push_notifications/src/push_notification_events.dart
  • lib/src/push_notifications/src/remote_message.dart
  • lib/src/realtime/src/connection_state_change.dart
  • lib/src/realtime/src/realtime_auth.dart
  • lib/src/realtime/src/realtime_channel_options.dart
  • lib/src/realtime/src/realtime_history_params.dart
  • lib/src/rest/src/rest_auth.dart
  • lib/src/rest/src/rest_history_params.dart
  • lib/src/rest/src/rest_presence_params.dart
  • pubspec.yaml
  • test/ably_event_listener_test.dart
  • test/ably_flutter_plugin_test.dart
  • test/mock_method_call_manager.dart
  • test/models/message_test.dart
  • test/models/presence_message_test.dart
  • test/realtime/channel_test.dart
  • test/rest/channel_test.dart
  • test/rest/channels_test.dart
  • test_integration/ios/.gitignore
  • test_integration/ios/Flutter/AppFrameworkInfo.plist
  • test_integration/ios/Podfile
  • test_integration/ios/Runner.xcodeproj/project.pbxproj
  • test_integration/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  • test_integration/ios/Runner/AppDelegate.swift
  • test_integration/ios/Runner/Info.plist
  • test_integration/lib/app_provisioning.dart
  • test_integration/lib/config/test_factory.dart
  • test_integration/lib/driver_data_handler.dart
  • test_integration/lib/factory/error_handler.dart
  • test_integration/lib/factory/reporter.dart
  • test_integration/lib/test/basic_test.dart
  • test_integration/lib/test/crypto/crypto_ensure_supported_key_length_test.dart
  • test_integration/lib/test/crypto/crypto_get_default_params.dart
  • test_integration/lib/test/realtime/realtime_auth_client_id_test.dart
  • test_integration/lib/test/realtime/realtime_auth_url_test.dart
  • test_integration/lib/test/realtime/realtime_authorize_test.dart
  • test_integration/lib/test/realtime/realtime_encrypted_publish_test.dart
  • test_integration/lib/test/realtime/realtime_events_test.dart
  • test_integration/lib/test/realtime/realtime_history_with_auth_callback_test.dart
  • test_integration/lib/test/realtime/realtime_presence_enter_update_leave.dart
  • test_integration/lib/test/realtime/realtime_presence_get.dart
  • test_integration/lib/test/realtime/realtime_presence_subscribe.dart
  • test_integration/lib/test/realtime/realtime_publish_test.dart
  • test_integration/lib/test/realtime/realtime_publish_with_auth_callback_test.dart
  • test_integration/lib/test/realtime/realtime_subscribe.dart
  • test_integration/lib/test/rest/rest_auth_client_id_test.dart
  • test_integration/lib/test/rest/rest_authorize_test.dart
  • test_integration/lib/test/rest/rest_capability_test.dart
  • test_integration/lib/test/rest/rest_create_token_request_test.dart
  • test_integration/lib/test/rest/rest_encrypted_publish_test.dart
  • test_integration/lib/test/rest/rest_history_test.dart
  • test_integration/lib/test/rest/rest_history_with_auth_callback_test.dart
  • test_integration/lib/test/rest/rest_presence_get_test.dart
  • test_integration/lib/test/rest/rest_presence_history_test.dart
  • test_integration/lib/test/rest/rest_publish_test.dart
  • test_integration/lib/test/rest/rest_publish_with_auth_callback_test.dart
  • test_integration/lib/test/rest/rest_request_token_test.dart
  • test_integration/lib/test/rest/rest_time_test.dart
  • test_integration/lib/test_dispatcher.dart
  • test_integration/lib/utils/data.dart
  • test_integration/lib/utils/encoders.dart
  • test_integration/lib/utils/realtime.dart
  • test_integration/lib/utils/rest.dart
  • test_integration/pubspec.yaml
  • test_integration/test_driver/runner.dart
  • test_integration/test_driver/test_implementation/basic_platform_tests.dart
  • test_integration/test_driver/test_implementation/crypto_tests.dart
  • test_integration/test_driver/test_implementation/helper_tests.dart
  • test_integration/test_driver/test_implementation/realtime_tests.dart
  • test_integration/test_driver/test_implementation/rest_tests.dart
  • test_integration/test_driver/test_implementation/utils.dart
  • test_integration/test_driver/tests_abstract.dart
  • test_integration/test_driver/tests_config.dart

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant