Skip to content

Commit b69bbc0

Browse files
authored
feat(messaging,ios): support early notification delegate setup for UIScene apps (#18501)
1 parent a56649a commit b69bbc0

4 files changed

Lines changed: 123 additions & 49 deletions

File tree

packages/firebase_messaging/firebase_messaging/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ To get started with Firebase Cloud Messaging for Flutter, please [see the docume
1616

1717
To use this plugin, please visit the [Cloud Messaging Usage documentation](https://firebase.google.com/docs/cloud-messaging)
1818

19+
### iOS apps using UIScene
20+
21+
Apps that adopt the UIScene lifecycle register Flutter plugins after
22+
`application:didFinishLaunchingWithOptions:`. Apple requires
23+
`UNUserNotificationCenter.delegate` to be configured before that method returns, so configure
24+
Firebase Messaging explicitly from your app delegate:
25+
26+
```objectivec
27+
#import <firebase_messaging/FLTFirebaseMessagingPlugin.h>
28+
29+
- (BOOL)application:(UIApplication *)application
30+
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
31+
[FLTFirebaseMessagingPlugin configureNotificationCenterDelegate];
32+
return [super application:application didFinishLaunchingWithOptions:launchOptions];
33+
}
34+
```
35+
1936
## Issues and feedback
2037

2138
Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/firebase/flutterfire/issues/new).

packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#import "AppDelegate.h"
2+
#import <firebase_messaging/FLTFirebaseMessagingPlugin.h>
23
#import "GeneratedPluginRegistrant.h"
34

45
@implementation AppDelegate
56

67
- (BOOL)application:(UIApplication *)application
78
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
8-
// Override point for customization after application launch.
9+
[FLTFirebaseMessagingPlugin configureNotificationCenterDelegate];
910
return [super application:application didFinishLaunchingWithOptions:launchOptions];
1011
}
1112

packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ @implementation FLTFirebaseMessagingPlugin {
4545

4646
// Guard against calling setupNotificationHandling twice
4747
BOOL _notificationHandlingSetup;
48+
BOOL _applicationObserverRegistered;
4849

4950
#if TARGET_OS_OSX
5051
// Tracks when plugin registration occurred after the macOS launch notification.
@@ -65,15 +66,39 @@ @implementation FLTFirebaseMessagingPlugin {
6566

6667
#pragma mark - FlutterPlugin
6768

68-
- (instancetype)initWithFlutterMethodChannel:(FlutterMethodChannel *)channel
69-
andFlutterPluginRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
69+
- (instancetype)init {
7070
self = [super init];
7171
if (self) {
7272
_initialNotificationGathered = NO;
7373
_sceneDidConnect = NO;
7474
_notificationHandlingSetup = NO;
75-
_channel = channel;
76-
_registrar = registrar;
75+
_applicationObserverRegistered = NO;
76+
}
77+
return self;
78+
}
79+
80+
+ (instancetype)sharedInstance {
81+
static FLTFirebaseMessagingPlugin *sharedInstance = nil;
82+
static dispatch_once_t onceToken;
83+
dispatch_once(&onceToken, ^{
84+
sharedInstance = [[FLTFirebaseMessagingPlugin alloc] init];
85+
});
86+
return sharedInstance;
87+
}
88+
89+
+ (void)configureNotificationCenterDelegate {
90+
#ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM
91+
[[FLTFirebaseMessagingPlugin sharedInstance] configureNotificationCenterDelegate];
92+
#endif
93+
}
94+
95+
- (void)configureWithFlutterMethodChannel:(FlutterMethodChannel *)channel
96+
andFlutterPluginRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
97+
_channel = channel;
98+
_registrar = registrar;
99+
100+
if (!_applicationObserverRegistered) {
101+
_applicationObserverRegistered = YES;
77102
// Application
78103
// Dart -> `getInitialNotification`
79104
// ObjC -> Initialize other delegates & observers
@@ -87,16 +112,14 @@ - (instancetype)initWithFlutterMethodChannel:(FlutterMethodChannel *)channel
87112
#endif
88113
object:nil];
89114
}
90-
return self;
91115
}
92116

93117
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
94118
FlutterMethodChannel *channel =
95119
[FlutterMethodChannel methodChannelWithName:kFLTFirebaseMessagingChannelName
96120
binaryMessenger:[registrar messenger]];
97-
FLTFirebaseMessagingPlugin *instance =
98-
[[FLTFirebaseMessagingPlugin alloc] initWithFlutterMethodChannel:channel
99-
andFlutterPluginRegistrar:registrar];
121+
FLTFirebaseMessagingPlugin *instance = [FLTFirebaseMessagingPlugin sharedInstance];
122+
[instance configureWithFlutterMethodChannel:channel andFlutterPluginRegistrar:registrar];
100123
// Register with internal FlutterFire plugin registry.
101124
[[FLTFirebasePluginRegistry sharedInstance] registerFirebasePlugin:instance];
102125

@@ -250,6 +273,57 @@ - (void)registerForRemoteNotifications {
250273
#endif
251274
}
252275

276+
#ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM
277+
- (void)configureNotificationCenterDelegate {
278+
// Set UNUserNotificationCenter but preserve original delegate if necessary.
279+
if (@available(iOS 10.0, macOS 10.14, *)) {
280+
BOOL shouldReplaceDelegate = YES;
281+
UNUserNotificationCenter *notificationCenter =
282+
[UNUserNotificationCenter currentNotificationCenter];
283+
id<UNUserNotificationCenterDelegate> currentDelegate = notificationCenter.delegate;
284+
285+
if (currentDelegate == self) {
286+
return;
287+
}
288+
289+
if (currentDelegate != nil) {
290+
#if !TARGET_OS_OSX
291+
// If a UNUserNotificationCenterDelegate is set and it conforms to
292+
// FlutterAppLifeCycleProvider then we don't want to replace it on iOS as the earlier
293+
// call to `[_registrar addApplicationDelegate:self];` will automatically delegate calls
294+
// to this plugin. If we replace it, it will cause a stack overflow as our original
295+
// delegate forwarding handler below causes an infinite loop of forwarding. See
296+
// https://github.com/firebasefire/issues/4026.
297+
if ([currentDelegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) {
298+
// Note this one only executes if Firebase swizzling is **enabled**.
299+
shouldReplaceDelegate = NO;
300+
}
301+
#endif
302+
303+
if (shouldReplaceDelegate) {
304+
_originalNotificationCenterDelegate = currentDelegate;
305+
_originalNotificationCenterDelegateRespondsTo.openSettingsForNotification =
306+
(unsigned int)[_originalNotificationCenterDelegate
307+
respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)];
308+
_originalNotificationCenterDelegateRespondsTo.willPresentNotification =
309+
(unsigned int)[_originalNotificationCenterDelegate
310+
respondsToSelector:@selector(userNotificationCenter:willPresentNotification:
311+
withCompletionHandler:)];
312+
_originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse =
313+
(unsigned int)[_originalNotificationCenterDelegate
314+
respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:
315+
withCompletionHandler:)];
316+
}
317+
}
318+
319+
if (shouldReplaceDelegate) {
320+
__strong FLTFirebasePlugin<UNUserNotificationCenterDelegate> *strongSelf = self;
321+
notificationCenter.delegate = strongSelf;
322+
}
323+
}
324+
}
325+
#endif
326+
253327
- (void)setupNotificationHandlingWithRemoteNotification:(nullable NSDictionary *)remoteNotification
254328
actionIdentifier:(nullable NSString *)actionIdentifier {
255329
// If notification handling was already set up (e.g. from
@@ -335,48 +409,10 @@ - (void)setupNotificationHandlingWithRemoteNotification:(nullable NSDictionary *
335409
[_registrar addApplicationDelegate:self];
336410
#endif
337411

338-
// Set UNUserNotificationCenter but preserve original delegate if necessary.
339-
if (@available(iOS 10.0, macOS 10.14, *)) {
340-
BOOL shouldReplaceDelegate = YES;
341-
UNUserNotificationCenter *notificationCenter =
342-
[UNUserNotificationCenter currentNotificationCenter];
343-
344-
if (notificationCenter.delegate != nil) {
345-
#if !TARGET_OS_OSX
346-
// If a UNUserNotificationCenterDelegate is set and it conforms to
347-
// FlutterAppLifeCycleProvider then we don't want to replace it on iOS as the earlier
348-
// call to `[_registrar addApplicationDelegate:self];` will automatically delegate calls
349-
// to this plugin. If we replace it, it will cause a stack overflow as our original
350-
// delegate forwarding handler below causes an infinite loop of forwarding. See
351-
// https://github.com/firebasefire/issues/4026.
352-
if ([notificationCenter.delegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) {
353-
// Note this one only executes if Firebase swizzling is **enabled**.
354-
shouldReplaceDelegate = NO;
355-
}
412+
#ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM
413+
[self configureNotificationCenterDelegate];
356414
#endif
357415

358-
if (shouldReplaceDelegate) {
359-
_originalNotificationCenterDelegate = notificationCenter.delegate;
360-
_originalNotificationCenterDelegateRespondsTo.openSettingsForNotification =
361-
(unsigned int)[_originalNotificationCenterDelegate
362-
respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)];
363-
_originalNotificationCenterDelegateRespondsTo.willPresentNotification =
364-
(unsigned int)[_originalNotificationCenterDelegate
365-
respondsToSelector:@selector(userNotificationCenter:willPresentNotification:
366-
withCompletionHandler:)];
367-
_originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse =
368-
(unsigned int)[_originalNotificationCenterDelegate
369-
respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:
370-
withCompletionHandler:)];
371-
}
372-
}
373-
374-
if (shouldReplaceDelegate) {
375-
__strong FLTFirebasePlugin<UNUserNotificationCenterDelegate> *strongSelf = self;
376-
notificationCenter.delegate = strongSelf;
377-
}
378-
}
379-
380416
// We automatically register for remote notifications as
381417
// application:didReceiveRemoteNotification:fetchCompletionHandler: will not get called unless
382418
// registerForRemoteNotifications is called early on during app initialization, calling this from

packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/include/FLTFirebaseMessagingPlugin.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
FIRMessagingDelegate,
4242
NSApplicationDelegate,
4343
UNUserNotificationCenterDelegate>
44+
45+
/// Returns the shared Firebase Messaging plugin instance.
46+
+ (instancetype)sharedInstance;
47+
48+
/// Configures Firebase Messaging as the `UNUserNotificationCenter` delegate.
49+
///
50+
/// Apps that adopt the UIScene lifecycle should call this from
51+
/// `application:didFinishLaunchingWithOptions:` before returning, because Apple requires the
52+
/// notification center delegate to be assigned before application launch completes.
53+
+ (void)configureNotificationCenterDelegate;
4454
#else
4555
@interface FLTFirebaseMessagingPlugin : FLTFirebasePlugin <FlutterPlugin,
4656
FLTFirebasePlugin,
@@ -59,6 +69,16 @@ API_AVAILABLE(ios(10.0))
5969
FlutterSceneLifeCycleDelegate
6070
#endif
6171
>
72+
73+
/// Returns the shared Firebase Messaging plugin instance.
74+
+ (instancetype)sharedInstance;
75+
76+
/// Configures Firebase Messaging as the `UNUserNotificationCenter` delegate.
77+
///
78+
/// Apps that adopt the UIScene lifecycle should call this from
79+
/// `application:didFinishLaunchingWithOptions:` before returning, because Apple requires the
80+
/// notification center delegate to be assigned before application launch completes.
81+
+ (void)configureNotificationCenterDelegate;
6282
#else
6383
@interface FLTFirebaseMessagingPlugin : FLTFirebasePlugin <FlutterPlugin,
6484
FLTFirebasePlugin,

0 commit comments

Comments
 (0)