From fcb74a9bc64b101040e9ab0f9cca17b9c30b5c1e Mon Sep 17 00:00:00 2001 From: Ingrid Wang Date: Mon, 16 Oct 2023 12:34:31 -0700 Subject: [PATCH 01/11] Convert deprecated UIUserNotificationType to UNAuthorizationOptions (#40884) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/40884 [Internal] - Removing usages of deprecated UIUserNotificationType Reviewed By: christophpurrer Differential Revision: D50238678 fbshipit-source-id: 16ee8cbec50a2273c0156f89691ef2ac7dea2d25 --- .../RCTPushNotificationManager.mm | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 7d575e2fe01de7..55a4b69562a7d3 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -401,33 +401,19 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification // Add a listener to make sure that startObserving has been called [self addListener:@"remoteNotificationsRegistered"]; -#if !TARGET_OS_OSX // [macOS - UIUserNotificationType types = UIUserNotificationTypeNone; + UNAuthorizationOptions options = UNAuthorizationOptionNone; if (permissions.alert()) { - types |= UIUserNotificationTypeAlert; + options |= UNAuthorizationOptionAlert; } if (permissions.badge()) { - types |= UIUserNotificationTypeBadge; + options |= UNAuthorizationOptionBadge; } if (permissions.sound()) { - types |= UIUserNotificationTypeSound; + options |= UNAuthorizationOptionSound; } -#else - NSRemoteNotificationType types = NSRemoteNotificationTypeNone; - - if (permissions.alert()) { - types |= NSRemoteNotificationTypeAlert; - } - if (permissions.badge()) { - types |= NSRemoteNotificationTypeBadge; - } - if (permissions.sound()) { - types |= NSRemoteNotificationTypeSound; - } -#endif // macOS] [UNUserNotificationCenter.currentNotificationCenter - requestAuthorizationWithOptions:types + requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *_Nullable error) { if (error != NULL) { reject(@"-1", @"Error - Push authorization request failed.", error); From 6a49629c2b8b00693ca9092e2f18715c90659f40 Mon Sep 17 00:00:00 2001 From: Ingrid Wang Date: Mon, 16 Oct 2023 17:01:16 -0700 Subject: [PATCH 02/11] Delete empty didRegisterUserNotificationSettings (#41013) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/41013 [iOS][Breaking] Deleted the no-op didRegisterUserNotificationSettings: callback in RCTPushNotificationManager Reviewed By: philIip Differential Revision: D50283620 fbshipit-source-id: 1582367c51c26e5b739cd9284d3b15bfa13274da --- .../PushNotificationIOS/RCTPushNotificationManager.h | 3 --- .../PushNotificationIOS/RCTPushNotificationManager.mm | 6 ------ packages/rn-tester/RNTester/AppDelegate.mm | 9 --------- 3 files changed, 18 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index fc912b4bef5cb8..f6fdb8c4e7e660 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -16,9 +16,6 @@ typedef void (^RCTRemoteNotificationCallback)(UIBackgroundFetchResult result); #endif // [macOS] #if !TARGET_OS_UIKITFORMAC -#if !TARGET_OS_OSX // [macOS] -+ (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; -#endif // [macOS] + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; + (void)didReceiveRemoteNotification:(NSDictionary *)notification; #if !TARGET_OS_OSX // [macOS] diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 55a4b69562a7d3..e4a249d553cff4 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -235,12 +235,6 @@ - (void)stopObserving ]; } -#if !TARGET_OS_OSX // [macOS] -+ (void)didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings *)notificationSettings -{ -} -#endif // [macOS] - + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSMutableString *hexString = [NSMutableString string]; diff --git a/packages/rn-tester/RNTester/AppDelegate.mm b/packages/rn-tester/RNTester/AppDelegate.mm index 7f98f5bcaa85a4..f40b76bd051bd7 100644 --- a/packages/rn-tester/RNTester/AppDelegate.mm +++ b/packages/rn-tester/RNTester/AppDelegate.mm @@ -338,15 +338,6 @@ - (void)registerPaperComponents:(NSArray *)components #if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC -#if !TARGET_OS_OSX // [macOS] -// Required to register for notifications -- (void)application:(__unused UIApplication *)application - didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings -{ - [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings]; -} -#endif // [macOS] - // Required for the remoteNotificationsRegistered event. - (void)application:(__unused RCTUIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken From 0aed385bc6fbc0eb6c91ea7cddf5dd4aa47f093c Mon Sep 17 00:00:00 2001 From: Ingrid Wang Date: Tue, 17 Oct 2023 13:11:20 -0700 Subject: [PATCH 03/11] Migrate cancel local notification methods to UserNotifications (#40949) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/40949 [Internal] Migrating cancelLocalNotifications and cancelAllLocalNotifications off of deprecated UILocalNotification methods Reviewed By: philIip, cipolleschi Differential Revision: D50275540 fbshipit-source-id: 3728e53b410322bbb0e269ac1407d197b8d5979f --- .../RCTPushNotificationManager.mm | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index e4a249d553cff4..97d5c2f9c60146 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -540,44 +540,43 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification RCT_EXPORT_METHOD(cancelAllLocalNotifications) { -#if !TARGET_OS_OSX // [macOS] - [RCTSharedApplication() cancelAllLocalNotifications]; -#else // [macOS - for (NSUserNotification *notif in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { - [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification:notif]; - } -#endif // macOS] + [[UNUserNotificationCenter currentNotificationCenter] + getPendingNotificationRequestsWithCompletionHandler:^(NSArray *requests) { + NSMutableArray *notificationIdentifiersToCancel = [NSMutableArray new]; + for (UNNotificationRequest *request in requests) { + [notificationIdentifiersToCancel addObject:request.identifier]; + } + [[UNUserNotificationCenter currentNotificationCenter] + removePendingNotificationRequestsWithIdentifiers:notificationIdentifiersToCancel]; + }]; } RCT_EXPORT_METHOD(cancelLocalNotifications : (NSDictionary *)userInfo) { -#if !TARGET_OS_OSX // [macOS] - for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { -#else // [macOS - for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { -#endif // macOS] - __block BOOL matchesAll = YES; - NSDictionary *notificationInfo = notification.userInfo; - // Note: we do this with a loop instead of just `isEqualToDictionary:` - // because we only require that all specified userInfo values match the - // notificationInfo values - notificationInfo may contain additional values - // which we don't care about. - [userInfo enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { - if (![notificationInfo[key] isEqual:obj]) { - matchesAll = NO; - *stop = YES; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray *_Nonnull requests) { + NSMutableArray *notificationIdentifiersToCancel = [NSMutableArray new]; + for (UNNotificationRequest *request in requests) { + NSDictionary *notificationInfo = request.content.userInfo; + // Note: we do this with a loop instead of just `isEqualToDictionary:` + // because we only require that all specified userInfo values match the + // notificationInfo values - notificationInfo may contain additional values + // which we don't care about. + __block BOOL shouldCancel = YES; + [userInfo enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if (![notificationInfo[key] isEqual:obj]) { + shouldCancel = NO; + *stop = YES; + } + }]; + + if (shouldCancel) { + [notificationIdentifiersToCancel addObject:request.identifier]; } - }]; -#if !TARGET_OS_OSX // [macOS] - if (matchesAll) { - [RCTSharedApplication() cancelLocalNotification:notification]; - } -#else // [macOS - if ([notification.identifier isEqualToString:userInfo[@"identifier"]] || matchesAll) { - [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification:notification]; } -#endif // macOS] - } + + [center removePendingNotificationRequestsWithIdentifiers:notificationIdentifiersToCancel]; + }]; } RCT_EXPORT_METHOD(getInitialNotification From b6a450a7584b91429400f7de37c45d059a31f24b Mon Sep 17 00:00:00 2001 From: Ingrid Wang Date: Wed, 18 Oct 2023 11:13:22 -0700 Subject: [PATCH 04/11] Migrate getScheduledLocalNotifications off of deprecated UILocalNotification (#40948) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/40948 [iOS][Breaking] alertAction is deprecated in PushNotificationIOS. getScheduledLocalNotifications now uses new iOS APIs which do not expose this property. Reviewed By: cipolleschi Differential Revision: D50275541 fbshipit-source-id: e4ecad858cd06350c749e7f5a837f36316656183 --- .../NativePushNotificationManagerIOS.js | 8 +- .../RCTPushNotificationManager.mm | 126 +++++++++++------- 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js b/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js index 102ce795e77a04..6d83970816aec8 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js @@ -23,14 +23,20 @@ type Notification = {| // Actual type: string | number +fireDate?: ?number, +alertBody?: ?string, - +alertAction?: ?string, +userInfo?: ?Object, +category?: ?string, // Actual type: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' +repeatInterval?: ?string, +applicationIconBadgeNumber?: ?number, +isSilent?: ?boolean, + /** + * Custom notification sound to play. Write-only: soundName will be null when + * accessing already created notifications using getScheduledLocalNotifications + * or getDeliveredNotifications. + */ +soundName?: ?string, + /** DEPRECATED. This was used for iOS's legacy UILocalNotification. */ + +alertAction?: ?string, |}; export interface Spec extends TurboModule { diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 97d5c2f9c60146..ca9b77942011b7 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -106,7 +106,11 @@ + (NSUserNotification *)NSUserNotification:(id)json } #endif // macOS] +@end + #if !TARGET_OS_OSX // [macOS] +@implementation RCTConvert (UIBackgroundFetchResult) + RCT_ENUM_CONVERTER( UIBackgroundFetchResult, (@{ @@ -116,9 +120,9 @@ + (NSUserNotification *)NSUserNotification:(id)json }), UIBackgroundFetchResultNoData, integerValue) -#endif // [macOS] @end +#endif // [macOS] #else @interface RCTPushNotificationManager () @end @@ -126,8 +130,10 @@ @interface RCTPushNotificationManager () @implementation RCTPushNotificationManager -#if !TARGET_OS_UIKITFORMAC && !TARGET_OS_OSX // [macOS] +#if !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_OSX // [macOS] +/** DEPRECATED. UILocalNotification was deprecated in iOS 10. Please don't add new callsites. */ static NSDictionary *RCTFormatLocalNotification(UILocalNotification *notification) { NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; @@ -146,41 +152,15 @@ @implementation RCTPushNotificationManager formattedLocalNotification[@"remote"] = @NO; return formattedLocalNotification; } - -static NSDictionary *RCTFormatUNNotification(UNNotification *notification) -{ - NSMutableDictionary *formattedNotification = [NSMutableDictionary dictionary]; - UNNotificationContent *content = notification.request.content; - - formattedNotification[@"identifier"] = notification.request.identifier; - - if (notification.date) { - NSDateFormatter *formatter = [NSDateFormatter new]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; - NSString *dateString = [formatter stringFromDate:notification.date]; - formattedNotification[@"date"] = dateString; - } - - formattedNotification[@"title"] = RCTNullIfNil(content.title); - formattedNotification[@"body"] = RCTNullIfNil(content.body); - formattedNotification[@"category"] = RCTNullIfNil(content.categoryIdentifier); - formattedNotification[@"thread-id"] = RCTNullIfNil(content.threadIdentifier); - formattedNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(content.userInfo)); - - return formattedNotification; -} - -#endif // TARGET_OS_UIKITFORMAC -#if TARGET_OS_OSX // [macOS - +#else // [macOS static NSDictionary *RCTFormatUserNotification(NSUserNotification *notification) { NSMutableDictionary *formattedUserNotification = [NSMutableDictionary dictionary]; if (notification.deliveryDate) { - NSDateFormatter *formatter = [NSDateFormatter new]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; - NSString *fireDateString = [formatter stringFromDate:notification.deliveryDate]; - formattedUserNotification[@"fireDate"] = fireDateString; + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + NSString *fireDateString = [formatter stringFromDate:notification.deliveryDate]; + formattedUserNotification[@"fireDate"] = fireDateString; } formattedUserNotification[@"alertAction"] = RCTNullIfNil(notification.actionButtonTitle); formattedUserNotification[@"alertBody"] = RCTNullIfNil(notification.informativeText); @@ -192,6 +172,60 @@ @implementation RCTPushNotificationManager } #endif // macOS] +/** For delivered notifications */ +static NSDictionary *RCTFormatUNNotification(UNNotification *notification) +{ + NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; + if (notification.date) { + formattedLocalNotification[@"fireDate"] = RCTFormatNotificationDateFromNSDate(notification.date); + } + [formattedLocalNotification addEntriesFromDictionary:RCTFormatUNNotificationContent(notification.request.content)]; + return formattedLocalNotification; +} + +/** For scheduled notification requests */ +static NSDictionary *RCTFormatUNNotificationRequest(UNNotificationRequest *request) +{ + NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; + if (request.trigger) { + NSDate *triggerDate = nil; + if ([request.trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) { + triggerDate = [(UNTimeIntervalNotificationTrigger *)request.trigger nextTriggerDate]; + } else if ([request.trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) { + triggerDate = [(UNCalendarNotificationTrigger *)request.trigger nextTriggerDate]; + } + + if (triggerDate) { + formattedLocalNotification[@"fireDate"] = RCTFormatNotificationDateFromNSDate(triggerDate); + } + } + [formattedLocalNotification addEntriesFromDictionary:RCTFormatUNNotificationContent(request.content)]; + return formattedLocalNotification; +} + +static NSDictionary *RCTFormatUNNotificationContent(UNNotificationContent *content) +{ + // Note: soundName is not set because this can't be read from UNNotificationSound. + // Note: alertAction is no longer relevant with UNNotification + NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; + formattedLocalNotification[@"alertTitle"] = RCTNullIfNil(content.title); + formattedLocalNotification[@"alertBody"] = RCTNullIfNil(content.body); + formattedLocalNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(content.userInfo)); + formattedLocalNotification[@"category"] = content.categoryIdentifier; + formattedLocalNotification[@"applicationIconBadgeNumber"] = content.badge; + formattedLocalNotification[@"remote"] = @NO; + return formattedLocalNotification; +} + +static NSString *RCTFormatNotificationDateFromNSDate(NSDate *date) +{ + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + return [formatter stringFromDate:date]; +} + +#endif // TARGET_OS_UIKITFORMAC + RCT_EXPORT_MODULE() - (dispatch_queue_t)methodQueue @@ -337,9 +371,9 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification [self sendEventWithName:@"remoteNotificationRegistrationError" body:errorDetails]; } -#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(onFinishRemoteNotification : (NSString *)notificationId fetchResult : (NSString *)fetchResult) { +#if !TARGET_OS_OSX // [macOS] UIBackgroundFetchResult result = [RCTConvert UIBackgroundFetchResult:fetchResult]; RCTRemoteNotificationCallback completionHandler = self.remoteNotificationCallbacks[notificationId]; if (!completionHandler) { @@ -348,8 +382,8 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } completionHandler(result); [self.remoteNotificationCallbacks removeObjectForKey:notificationId]; -} #endif // [macOS] +} /** * Update the application icon badge number on the home screen @@ -610,20 +644,14 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification RCT_EXPORT_METHOD(getScheduledLocalNotifications : (RCTResponseSenderBlock)callback) { -#if !TARGET_OS_OSX // [macOS] - NSArray *scheduledLocalNotifications = RCTSharedApplication().scheduledLocalNotifications; -#endif // [macOS] - NSMutableArray *formattedScheduledLocalNotifications = [NSMutableArray new]; -#if !TARGET_OS_OSX // [macOS] - for (UILocalNotification *notification in scheduledLocalNotifications) { - [formattedScheduledLocalNotifications addObject:RCTFormatLocalNotification(notification)]; - } -#else // [macOS - for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { - [formattedScheduledLocalNotifications addObject:RCTFormatUserNotification(notification)]; - } -#endif // macOS] - callback(@[ formattedScheduledLocalNotifications ]); + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray *_Nonnull requests) { + NSMutableArray *formattedScheduledLocalNotifications = [NSMutableArray new]; + for (UNNotificationRequest *request in requests) { + [formattedScheduledLocalNotifications addObject:RCTFormatUNNotificationRequest(request)]; + } + callback(@[ formattedScheduledLocalNotifications ]); + }]; } RCT_EXPORT_METHOD(removeAllDeliveredNotifications) From 8e59fd1ba358ed7bd9c14f9f2ef647f0bf072068 Mon Sep 17 00:00:00 2001 From: Ingrid Wang Date: Fri, 20 Oct 2023 13:22:55 -0700 Subject: [PATCH 05/11] Migrate notification scheduling to UserNotifications (#41039) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/41039 [iOS][Breaking] - repeatInterval is deprecated in PushNotificationIOS. Use fireDate and the new fireIntervalSeconds. Reviewed By: philIip Differential Revision: D50277316 fbshipit-source-id: ddcc2d2fc9d89d2bacac296848109e98c95c0107 --- .../NativePushNotificationManagerIOS.js | 30 +++- .../RCTPushNotificationManager.mm | 166 +++++++----------- 2 files changed, 93 insertions(+), 103 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js b/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js index 6d83970816aec8..0df32a8b3792ea 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js @@ -20,14 +20,34 @@ type Permissions = {| type Notification = {| +alertTitle?: ?string, - // Actual type: string | number - +fireDate?: ?number, +alertBody?: ?string, +userInfo?: ?Object, + /** + * Identifier for the notification category. See the [Apple documentation](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types) + * for more details. + */ +category?: ?string, - // Actual type: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' - +repeatInterval?: ?string, + /** + * Actual type: string | number + * + * Schedule notifications using EITHER `fireDate` or `fireIntervalSeconds`. + * If both are specified, `fireDate` takes precedence. + * If you use `presentLocalNotification`, both will be ignored + * and the notification will be shown immediately. + */ + +fireDate?: ?number, + /** + * Seconds from now to display the notification. + * + * Schedule notifications using EITHER `fireDate` or `fireIntervalSeconds`. + * If both are specified, `fireDate` takes precedence. + * If you use `presentLocalNotification`, both will be ignored + * and the notification will be shown immediately. + */ + +fireIntervalSeconds?: ?number, + /** Badge count to display on the app icon. */ +applicationIconBadgeNumber?: ?number, + /** Whether to silence the notification sound. */ +isSilent?: ?boolean, /** * Custom notification sound to play. Write-only: soundName will be null when @@ -37,6 +57,8 @@ type Notification = {| +soundName?: ?string, /** DEPRECATED. This was used for iOS's legacy UILocalNotification. */ +alertAction?: ?string, + /** DEPRECATED. Use `fireDate` or `fireIntervalSeconds` instead. */ + +repeatInterval?: ?string, |}; export interface Spec extends TurboModule { diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index ca9b77942011b7..de14efc7630d0b 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -25,86 +25,60 @@ static NSString *const kErrorUnableToRequestPermissions = @"E_UNABLE_TO_REQUEST_PERMISSIONS"; #if !TARGET_OS_UIKITFORMAC -@implementation RCTConvert (NSCalendarUnit) - -RCT_ENUM_CONVERTER( - NSCalendarUnit, - (@{ - @"year" : @(NSCalendarUnitYear), - @"month" : @(NSCalendarUnitMonth), - @"week" : @(NSCalendarUnitWeekOfYear), - @"day" : @(NSCalendarUnitDay), - @"hour" : @(NSCalendarUnitHour), - @"minute" : @(NSCalendarUnitMinute) - }), - 0, - integerValue) - -@end @interface RCTPushNotificationManager () @property (nonatomic, strong) NSMutableDictionary *remoteNotificationCallbacks; @end -@implementation RCTConvert (UILocalNotification) +@implementation RCTConvert (UNNotificationContent) -#if !TARGET_OS_OSX // [macOS] -+ (UILocalNotification *)UILocalNotification:(id)json ++ (UNNotificationContent *)UNNotificationContent:(id)json { NSDictionary *details = [self NSDictionary:json]; BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]]; - UILocalNotification *notification = [UILocalNotification new]; - notification.alertTitle = [RCTConvert NSString:details[@"alertTitle"]]; - notification.fireDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; - notification.alertBody = [RCTConvert NSString:details[@"alertBody"]]; - notification.alertAction = [RCTConvert NSString:details[@"alertAction"]]; - notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; - notification.category = [RCTConvert NSString:details[@"category"]]; - notification.repeatInterval = [RCTConvert NSCalendarUnit:details[@"repeatInterval"]]; + UNMutableNotificationContent *content = [UNMutableNotificationContent new]; + content.title = [RCTConvert NSString:details[@"alertTitle"]]; + content.body = [RCTConvert NSString:details[@"alertBody"]]; + content.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; + content.categoryIdentifier = [RCTConvert NSString:details[@"category"]]; if (details[@"applicationIconBadgeNumber"]) { - notification.applicationIconBadgeNumber = [RCTConvert NSInteger:details[@"applicationIconBadgeNumber"]]; + content.badge = [RCTConvert NSNumber:details[@"applicationIconBadgeNumber"]]; } if (!isSilent) { - notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: UILocalNotificationDefaultSoundName; + NSString *soundName = [RCTConvert NSString:details[@"soundName"]]; + content.sound = + soundName ? [UNNotificationSound soundNamed:details[@"soundName"]] : [UNNotificationSound defaultSound]; } - return notification; + + return content; } -#else // [macOS -+ (NSUserNotification *)NSUserNotification:(id)json + ++ (NSDictionary *)NSDictionaryForNotification: + (JS::NativePushNotificationManagerIOS::Notification &)notification { - NSDictionary *details = [self NSDictionary:json]; - BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]]; - NSUserNotification *notification = [NSUserNotification new]; - notification.deliveryDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; - notification.informativeText = [RCTConvert NSString:details[@"alertBody"]]; - NSString *title = [RCTConvert NSString:details[@"alertTitle"]]; - if (title) { - notification.title = title; - } - NSString *actionButtonTitle = [RCTConvert NSString:details[@"alertAction"]]; - if (actionButtonTitle) { - notification.actionButtonTitle = actionButtonTitle; + // Note: alertAction is not set, as it is no longer relevant with UNNotification + NSMutableDictionary *notificationDict = [NSMutableDictionary new]; + notificationDict[@"alertTitle"] = notification.alertTitle(); + notificationDict[@"alertBody"] = notification.alertBody(); + notificationDict[@"userInfo"] = notification.userInfo(); + notificationDict[@"category"] = notification.category(); + if (notification.fireIntervalSeconds()) { + notificationDict[@"fireIntervalSeconds"] = @(*notification.fireIntervalSeconds()); } - notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; - - NSCalendarUnit calendarUnit = [RCTConvert NSCalendarUnit:details[@"repeatInterval"]]; - if (calendarUnit > 0) { - NSDateComponents *dateComponents = [NSDateComponents new]; - [dateComponents setValue:1 forComponent:calendarUnit]; - notification.deliveryRepeatInterval = dateComponents; + if (notification.fireDate()) { + notificationDict[@"fireDate"] = @(*notification.fireDate()); } - if (!isSilent) { - notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: NSUserNotificationDefaultSoundName; + if (notification.applicationIconBadgeNumber()) { + notificationDict[@"applicationIconBadgeNumber"] = @(*notification.applicationIconBadgeNumber()); } - - NSString *identifier = [RCTConvert NSString:details[@"identifier"]]; - if (identifier == nil) { - identifier = [[NSUUID UUID] UUIDString]; + if (notification.isSilent()) { + notificationDict[@"isSilent"] = @(*notification.isSilent()); + if ([notificationDict[@"isSilent"] isEqualToNumber:@(NO)]) { + notificationDict[@"soundName"] = notification.soundName(); + } } - notification.identifier = identifier; - return notification; + return notificationDict; } -#endif // macOS] @end @@ -513,26 +487,16 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification #if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(presentLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { - NSMutableDictionary *notificationDict = [NSMutableDictionary new]; - notificationDict[@"alertTitle"] = notification.alertTitle(); - notificationDict[@"alertBody"] = notification.alertBody(); - notificationDict[@"alertAction"] = notification.alertAction(); - notificationDict[@"userInfo"] = notification.userInfo(); - notificationDict[@"category"] = notification.category(); - notificationDict[@"repeatInterval"] = notification.repeatInterval(); - if (notification.fireDate()) { - notificationDict[@"fireDate"] = @(*notification.fireDate()); - } - if (notification.applicationIconBadgeNumber()) { - notificationDict[@"applicationIconBadgeNumber"] = @(*notification.applicationIconBadgeNumber()); - } - if (notification.isSilent()) { - notificationDict[@"isSilent"] = @(*notification.isSilent()); - if ([notificationDict[@"isSilent"] isEqualToNumber:@(NO)]) { - notificationDict[@"soundName"] = notification.soundName(); - } - } - [RCTSharedApplication() presentLocalNotificationNow:[RCTConvert UILocalNotification:notificationDict]]; + NSDictionary *notificationDict = [RCTConvert NSDictionaryForNotification:notification]; + UNNotificationContent *content = [RCTConvert UNNotificationContent:notificationDict]; + UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 + repeats:NO]; + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] + content:content + trigger:trigger]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:request withCompletionHandler:nil]; } #else // [macOS RCT_EXPORT_METHOD(presentLocalNotification:(NSUserNotification *)notification) @@ -544,26 +508,30 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification #if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(scheduleLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { - NSMutableDictionary *notificationDict = [NSMutableDictionary new]; - notificationDict[@"alertTitle"] = notification.alertTitle(); - notificationDict[@"alertBody"] = notification.alertBody(); - notificationDict[@"alertAction"] = notification.alertAction(); - notificationDict[@"userInfo"] = notification.userInfo(); - notificationDict[@"category"] = notification.category(); - notificationDict[@"repeatInterval"] = notification.repeatInterval(); - if (notification.fireDate()) { - notificationDict[@"fireDate"] = @(*notification.fireDate()); + NSDictionary *notificationDict = [RCTConvert NSDictionaryForNotification:notification]; + UNNotificationContent *content = [RCTConvert UNNotificationContent:notificationDict]; + + UNNotificationTrigger *trigger = nil; + if (notificationDict[@"fireDate"]) { + NSDate *fireDate = [RCTConvert NSDate:notificationDict[@"fireDate"]] ?: [NSDate date]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDateComponents *components = + [calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | + NSCalendarUnitMinute | NSCalendarUnitSecond) + fromDate:fireDate]; + trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:NO]; + } else if (notificationDict[@"fireIntervalSeconds"]) { + trigger = [UNTimeIntervalNotificationTrigger + triggerWithTimeInterval:[notificationDict[@"fireIntervalSeconds"] doubleValue] + repeats:NO]; } - if (notification.applicationIconBadgeNumber()) { - notificationDict[@"applicationIconBadgeNumber"] = @(*notification.applicationIconBadgeNumber()); - } - if (notification.isSilent()) { - notificationDict[@"isSilent"] = @(*notification.isSilent()); - if ([notificationDict[@"isSilent"] isEqualToNumber:@(NO)]) { - notificationDict[@"soundName"] = notification.soundName(); - } - } - [RCTSharedApplication() scheduleLocalNotification:[RCTConvert UILocalNotification:notificationDict]]; + + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] + content:content + trigger:trigger]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:request withCompletionHandler:nil]; } #else // [macOS RCT_EXPORT_METHOD(scheduleLocalNotification:(NSUserNotification *)notification) From 4ac3d3735dd31a0c5869f0cc7e724b82c9c4d52a Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 13 Jan 2024 14:57:44 -0800 Subject: [PATCH 06/11] Remove most of the remaining macOS ifdef blocks from RCTPushNotificationManager --- .../RCTPushNotificationManager.mm | 56 +++---------------- 1 file changed, 9 insertions(+), 47 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index de14efc7630d0b..c0c08ab746c853 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -131,10 +131,10 @@ @implementation RCTPushNotificationManager { NSMutableDictionary *formattedUserNotification = [NSMutableDictionary dictionary]; if (notification.deliveryDate) { - NSDateFormatter *formatter = [NSDateFormatter new]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; - NSString *fireDateString = [formatter stringFromDate:notification.deliveryDate]; - formattedUserNotification[@"fireDate"] = fireDateString; + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + NSString *fireDateString = [formatter stringFromDate:notification.deliveryDate]; + formattedUserNotification[@"fireDate"] = fireDateString; } formattedUserNotification[@"alertAction"] = RCTNullIfNil(notification.actionButtonTitle); formattedUserNotification[@"alertBody"] = RCTNullIfNil(notification.informativeText); @@ -280,16 +280,16 @@ + (void)didReceiveRemoteNotification:(NSDictionary *)notification object:self userInfo:userInfo]; } +#endif // [macOS] +#if !TARGET_OS_OSX // [macOS] + (void)didReceiveLocalNotification:(UILocalNotification *)notification { [[NSNotificationCenter defaultCenter] postNotificationName:kLocalNotificationReceived object:self userInfo:RCTFormatLocalNotification(notification)]; } - -#else // [macOS - +#else // [macOS + (void)didReceiveUserNotification:(NSUserNotification *)notification { NSString *notificationName = notification.isRemote ? RCTRemoteNotificationReceived : kLocalNotificationReceived; @@ -298,7 +298,6 @@ + (void)didReceiveUserNotification:(NSUserNotification *)notification object:self userInfo:userInfo]; } - #endif // macOS] - (void)handleLocalNotificationReceived:(NSNotification *)notification @@ -308,15 +307,13 @@ - (void)handleLocalNotificationReceived:(NSNotification *)notification - (void)handleRemoteNotificationReceived:(NSNotification *)notification { +#if !TARGET_OS_OSX // [macOS] NSMutableDictionary *remoteNotification = [NSMutableDictionary dictionaryWithDictionary:notification.userInfo[@"notification"]]; -#if !TARGET_OS_OSX // [macOS] RCTRemoteNotificationCallback completionHandler = notification.userInfo[@"completionHandler"]; -#endif // [macOS] NSString *notificationId = [[NSUUID UUID] UUIDString]; remoteNotification[@"notificationId"] = notificationId; remoteNotification[@"remote"] = @YES; -#if !TARGET_OS_OSX // [macOS] if (completionHandler) { if (!self.remoteNotificationCallbacks) { // Lazy initialization @@ -324,9 +321,9 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification } self.remoteNotificationCallbacks[notificationId] = completionHandler; } -#endif // [macOS] [self sendEventWithName:@"remoteNotificationReceived" body:remoteNotification]; +#endif // [macOS] } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification @@ -484,7 +481,6 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification }; } -#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(presentLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { NSDictionary *notificationDict = [RCTConvert NSDictionaryForNotification:notification]; @@ -498,14 +494,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:request withCompletionHandler:nil]; } -#else // [macOS -RCT_EXPORT_METHOD(presentLocalNotification:(NSUserNotification *)notification) -{ - [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; -} -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(scheduleLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { NSDictionary *notificationDict = [RCTConvert NSDictionaryForNotification:notification]; @@ -533,12 +522,6 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:request withCompletionHandler:nil]; } -#else // [macOS -RCT_EXPORT_METHOD(scheduleLocalNotification:(NSUserNotification *)notification) -{ - [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; -} -#endif // macOS] RCT_EXPORT_METHOD(cancelAllLocalNotifications) { @@ -624,32 +607,18 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { -#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeAllDeliveredNotifications]; -#else // [macOS - [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications]; -#endif // macOS] } RCT_EXPORT_METHOD(removeDeliveredNotifications : (NSArray *)identifiers) { -#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeDeliveredNotificationsWithIdentifiers:identifiers]; -#else // [macOS - NSArray *notificationsToRemove = [[NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSUserNotification* evaluatedObject, NSDictionary * _Nullable bindings) { - return [identifiers containsObject:evaluatedObject.identifier]; - }]]; - for (NSUserNotification *notification in notificationsToRemove) { - [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:notification]; - } -#endif // macOS] } RCT_EXPORT_METHOD(getDeliveredNotifications : (RCTResponseSenderBlock)callback) { -#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getDeliveredNotificationsWithCompletionHandler:^(NSArray *_Nonnull notifications) { NSMutableArray *formattedNotifications = [NSMutableArray new]; @@ -659,13 +628,6 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } callback(@[ formattedNotifications ]); }]; -#else // [macOS - NSMutableArray *formattedNotifications = [NSMutableArray new]; - for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications) { - [formattedNotifications addObject:RCTFormatUserNotification(notification)]; - } - callback(@[formattedNotifications]); -#endif // macOS] } RCT_EXPORT_METHOD(getAuthorizationStatus : (RCTResponseSenderBlock)callback) From 1594a6ad4679e9ee4489b73832c96282771ef9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Fri, 27 Oct 2023 11:11:19 -0700 Subject: [PATCH 07/11] feat(iOS): migrate deprecated UIMenuController to UIEditMenuInteraction (#41125) Summary: The goal of this PR is to migrate deprecated `UIMenuController` to `UIEditMenuInteraction`. `UIMenuController` has been deprecated in iOS 16 and for that reason it's not available for VisionOS. https://github.com/facebook/react-native/assets/52801365/fed994be-d444-462a-9ed0-39b50531425d bypass-github-export-checks [IOS] [CHANGED] - Migrate RCTTextView to UIEditMenuInteraction Pull Request resolved: https://github.com/facebook/react-native/pull/41125 Test Plan: Launch RNTester and check for "Selectable Text" example and check that it works for iOS 16/17. Reviewed By: javache Differential Revision: D50551016 Pulled By: cipolleschi fbshipit-source-id: 558ecc5a04a5daa9c4360fabddcab28fba72a323 --- .../Libraries/Text/Text/RCTTextView.m | 23 ++++++++++++++++- .../Text/RCTParagraphComponentView.mm | 25 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.m b/packages/react-native/Libraries/Text/Text/RCTTextView.m index 9ac677b4e90b3d..f8934d1a353b49 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.m +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.m @@ -37,7 +37,16 @@ - (BOOL)canBecomeKeyView @end +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] +@interface RCTTextView () + +@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); +#else // [macOS @interface RCTTextView () +// macOS] + @end #endif // macOS] @@ -358,6 +367,10 @@ - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + if (@available(iOS 16.0, *)) { + _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self]; + [self addInteraction:_editMenuInteraction]; + } [self addGestureRecognizer:_longPressGestureRecognizer]; } @@ -369,8 +382,16 @@ - (void)disableContextMenu - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { - // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) #if !TARGET_OS_UIKITFORMAC + if (@available(iOS 16.0, *)) { + CGPoint location = [gesture locationInView:self]; + UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:location]; + if (_editMenuInteraction) { + [_editMenuInteraction presentEditMenuWithConfiguration:config]; + } + return; + } + // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController.isMenuVisible) { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index eddff908cfaf88..5ded35c6dedce9 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -27,6 +27,12 @@ using namespace facebook::react; +@interface RCTParagraphComponentView () + +@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); + +@end + @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; @@ -223,19 +229,36 @@ - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + + if (@available(iOS 16.0, *)) { + _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self]; + [self addInteraction:_editMenuInteraction]; + } [self addGestureRecognizer:_longPressGestureRecognizer]; } - (void)disableContextMenu { [self removeGestureRecognizer:_longPressGestureRecognizer]; + if (@available(iOS 16.0, *)) { + [self removeInteraction:_editMenuInteraction]; + _editMenuInteraction = nil; + } _longPressGestureRecognizer = nil; } - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { - // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) #if !TARGET_OS_UIKITFORMAC + if (@available(iOS 16.0, *)) { + CGPoint location = [gesture locationInView:self]; + UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:location]; + if (_editMenuInteraction) { + [_editMenuInteraction presentEditMenuWithConfiguration:config]; + } + return; + } + // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController.isMenuVisible) { From f02219c725a695bbc658dac984a7776db40e9899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Tue, 31 Oct 2023 17:57:56 -0700 Subject: [PATCH 08/11] feat(iOS): remove usages of UIScreen mainScreen for Trait collections (#41214) Summary: The goal of this PR is to migrate from deprecated `[UIScreen mainScreen]` and get the `displayScale` from currentTraitCollection. Both of those return the same values. [IOS] [CHANGED] - retrieve screen scale from trait collection instead of UIScreen mainScreen Pull Request resolved: https://github.com/facebook/react-native/pull/41214 Test Plan: Go to Dimensions example and check that everything works as expected Reviewed By: NickGerleman Differential Revision: D50736794 Pulled By: javache fbshipit-source-id: d512cba1120204be95caf43ac9916f6597e2ccc8 --- packages/react-native/React/Base/RCTUtils.m | 4 ++-- .../Mounting/ComponentViews/View/RCTViewComponentView.mm | 2 +- packages/react-native/React/Views/RCTViewManager.m | 2 +- .../rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index 2ba69e0d0b6720..9f7d49c9e6ed01 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -305,14 +305,14 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp void RCTComputeScreenScale() { dispatch_once(&onceTokenScreenScale, ^{ - screenScale = [UIScreen mainScreen].scale; + screenScale = [UITraitCollection currentTraitCollection].displayScale; }); } CGFloat RCTScreenScale() { RCTUnsafeExecuteOnMainQueueOnceSync(&onceTokenScreenScale, ^{ - screenScale = [UIScreen mainScreen].scale; + screenScale = [UITraitCollection currentTraitCollection].displayScale; }); return screenScale; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index e19a16c9fdd165..02991f8a8850e6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -255,7 +255,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) { self.layer.shouldRasterize = newViewProps.shouldRasterize; #if !TARGET_OS_OSX // [macOS] - self.layer.rasterizationScale = newViewProps.shouldRasterize ? [UIScreen mainScreen].scale : 1.0; + self.layer.rasterizationScale = newViewProps.shouldRasterize ? self.traitCollection.displayScale : 1.0; #else // [macOS self.layer.rasterizationScale = 1.0; #endif // macOS] diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index b2bd47511ce1e6..4dc0f45004614e 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -235,7 +235,7 @@ - (RCTShadowView *)shadowView { view.layer.shouldRasterize = json ? [RCTConvert BOOL:json] : defaultView.layer.shouldRasterize; view.layer.rasterizationScale = - view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale; + view.layer.shouldRasterize ? view.traitCollection.displayScale : defaultView.layer.rasterizationScale; } #endif // [macOS] diff --git a/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m b/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m index 964efed23099cb..9aa90fa83c6e0d 100644 --- a/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m +++ b/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m @@ -45,14 +45,13 @@ - (BOOL)compareWithImage:(UIImage *)image (CGBitmapInfo)kCGImageAlphaPremultipliedLast); #if !TARGET_OS_OSX // [macOS] - CGFloat scaleFactor = [UIScreen mainScreen].scale; + CGFloat scaleFactor = [UITraitCollection currentTraitCollection].displayScale; #else // [macOS // The compareWithImage: method is used for integration test snapshot image comparison. // The _snapshotView: method that creates snapshot images that are *not* scaled for the screen. // By not using the screen scale factor in this method the test results are machine independent. CGFloat scaleFactor = 1; #endif // macOS] - CGContextScaleCTM(referenceImageContext, scaleFactor, scaleFactor); CGContextScaleCTM(imageContext, scaleFactor, scaleFactor); From 762f559b261f905076c8bad29a10d8d83460c907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Fri, 10 Nov 2023 05:03:26 -0800 Subject: [PATCH 09/11] feat(iOS) remove deprecated [UIScreen mainScreen] references (#41388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: The goal for this PR is to further remove references for `[UIScreen mainScreen]` and migrate them to use trait collections. This helps out of tree platforms like visionOS (where the `UIScreen` is not available). bypass-github-export-checks [INTERNAL] [CHANGED] - use currentTraitCollection for FBSnapshotTestController.m [IOS] [CHANGED] - use key window width to assign the correct width for RCTDevLoadingView Pull Request resolved: https://github.com/facebook/react-native/pull/41388 Test Plan: – Check if tests passes - Check if `RCTDevLoadingView` shows up correctly. Screenshot: ![CleanShot 2023-11-09 at 13 48 48@2x](https://github.com/facebook/react-native/assets/52801365/4c91399e-f70a-4e78-8288-bc7b8377c980) Reviewed By: javache Differential Revision: D51156230 Pulled By: cipolleschi fbshipit-source-id: bbe711e0281046a082fd1680b55e2d117915ad00 --- .../React/CoreModules/RCTDevLoadingView.mm | 10 ++++------ .../FBSnapshotTestCase/FBSnapshotTestController.m | 13 +++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 4d7d3725003f48..30d2d56862c0a5 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -141,13 +141,11 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo self->_showDate = [NSDate date]; if (!self->_window && !RCTRunningInTestEnvironment()) { #if !TARGET_OS_OSX // [macOS] - CGSize screenSize = [UIScreen mainScreen].bounds.size; - UIWindow *window = RCTSharedApplication().keyWindow; - self->_window = - [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, screenSize.width, window.safeAreaInsets.top + 10)]; - self->_label = - [[UILabel alloc] initWithFrame:CGRectMake(0, window.safeAreaInsets.top - 10, screenSize.width, 20)]; + CGFloat windowWidth = window.bounds.size.width; + + self->_window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, windowWidth, window.safeAreaInsets.top + 10)]; + self->_label = [[UILabel alloc] initWithFrame:CGRectMake(0, window.safeAreaInsets.top - 10, windowWidth, 20)]; [self->_window addSubview:self->_label]; self->_window.windowLevel = UIWindowLevelStatusBar + 1; diff --git a/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m b/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m index 266502d69ca6d1..c7e36bf0232c26 100644 --- a/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m +++ b/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m @@ -240,14 +240,15 @@ - (NSString *)_fileNameForSelector:(SEL)selector if (0 < identifier.length) { fileName = [fileName stringByAppendingFormat:@"_%@", identifier]; } - CGFloat scale; // [macOS -#if !TARGET_OS_OSX - scale = [[UIScreen mainScreen] scale]; -#else +#if !TARGET_OS_OSX // [macOS] + UITraitCollection *currentTraitCollection = [UITraitCollection currentTraitCollection]; + if (currentTraitCollection.displayScale > 1.0) { + fileName = [fileName stringByAppendingFormat:@"@%.fx", currentTraitCollection.displayScale]; +#else // [macOS scale = [[NSScreen mainScreen] backingScaleFactor]; -#endif - if (scale > 1.0) { // macOS] + if (scale > 1.0) { fileName = [fileName stringByAppendingFormat:@"@%.fx", scale]; +#endif // macOS] } fileName = [fileName stringByAppendingPathExtension:@"png"]; return fileName; From a0c42946c7bcf4777cfa500052fb5e0b9478138e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Tue, 19 Dec 2023 10:09:49 -0800 Subject: [PATCH 10/11] feat: Optimise RCTKeyWindow() calls in RCTForceTouchAvailable method (#41935) Summary: This PR optimises RCTKeyWindow() calls in `RCTForceTouchAvailable` method. This method was calling RCTKeyWindow hundreds of times while scrolling on the screen. Before: On the video you can see that this function is being called **350 times** just from simple list scrolling. RCTKeyWindow is looping over app windows so it's not a cheap operation. https://github.com/facebook/react-native/assets/52801365/5b69cbd6-d148-4d06-b672-bd7b60472c13 After: the function is called only few times at the start of the app to get initial layout measurements. Solution: I think we can check just once for the force touch capabilities as devices can't change it on the fly bypass-github-export-checks ## Changelog: [IOS] [FIXED] - Optimise RCTKeyWindow() calls in RCTForceTouchAvailable method Pull Request resolved: https://github.com/facebook/react-native/pull/41935 Test Plan: CI Green Reviewed By: dmytrorykun Differential Revision: D52172510 Pulled By: cipolleschi fbshipit-source-id: 881a3125a2af4376ce65d785d8eee09c7d8f1f16 --- packages/react-native/React/Base/RCTUtils.m | 6 ++---- packages/react-native/React/UIUtils/RCTUIUtils.m | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index 9f7d49c9e6ed01..0a83e19b3a2121 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -630,12 +630,10 @@ BOOL RCTForceTouchAvailable(void) static BOOL forceSupported; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - forceSupported = - [UITraitCollection class] && [UITraitCollection instancesRespondToSelector:@selector(forceTouchCapability)]; + forceSupported = [UITraitCollection currentTraitCollection].forceTouchCapability == UIForceTouchCapabilityAvailable; }); - return forceSupported && - (RCTKeyWindow() ?: [UIView new]).traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable; + return forceSupported; } #endif // [macOS] diff --git a/packages/react-native/React/UIUtils/RCTUIUtils.m b/packages/react-native/React/UIUtils/RCTUIUtils.m index fdac0db4c06432..834dd103f77ac0 100644 --- a/packages/react-native/React/UIUtils/RCTUIUtils.m +++ b/packages/react-native/React/UIUtils/RCTUIUtils.m @@ -15,8 +15,7 @@ RCTDimensions RCTGetDimensions(CGFloat fontScale) UIScreen *mainScreen = UIScreen.mainScreen; CGSize screenSize = mainScreen.bounds.size; - UIView *mainWindow; - mainWindow = RCTKeyWindow(); + UIView *mainWindow = RCTKeyWindow(); // We fallback to screen size if a key window is not found. CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize; From f62ccc8e3d783acdb7ffdf8847d5cc40f39dd9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Fri, 5 Jan 2024 07:04:55 -0800 Subject: [PATCH 11/11] feat: refactor `RCTKeyWindow` to be more resilient and work in multi-window apps (#42036) Summary: This PRs refactors `RCTKeyWindow()` to be more resilient and work in multi-window apps. After my recent PR got merged (https://github.com/facebook/react-native/issues/41935) it significantly reduced the number of calls to `RCTKeyWindow()` and now it's called only when necessary. So this PR makes this function a bit more resource intensive but it guarantees that we will find current scene's key window. This would also fix some brownfield scenarios where React Native is working in multi-window mode and in the future allow us to more easily adopt `UIWindowSceneDelegate` bypass-github-export-checks [IOS] [CHANGED] - refactor `RCTKeyWindow` to be more resilient and work in multi-window apps Pull Request resolved: https://github.com/facebook/react-native/pull/42036 Test Plan: Checkout RNTester example for Alerts and LoadingView. https://github.com/facebook/react-native/assets/52801365/8cf4d698-db6d-4a12-8d8d-7a5acf34858b Reviewed By: huntie Differential Revision: D52431720 Pulled By: cipolleschi fbshipit-source-id: 0d6ef1d46b2428c30c9f64dae66b95dbc69f0a3b --- packages/react-native/React/Base/RCTUtils.m | 16 +++++++++--- .../React/CoreModules/RCTAlertController.m | 25 +------------------ .../React/CoreModules/RCTDevLoadingView.mm | 14 ++++------- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index 0a83e19b3a2121..f741dcf613b020 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -600,12 +600,20 @@ BOOL RCTRunningInAppExtension(void) return nil; } - // TODO: replace with a more robust solution - for (UIWindow *window in RCTSharedApplication().windows) { - if (window.keyWindow) { - return window; + for (UIScene *scene in RCTSharedApplication().connectedScenes) { + if (scene.activationState != UISceneActivationStateForegroundActive || + ![scene isKindOfClass:[UIWindowScene class]]) { + continue; + } + UIWindowScene *windowScene = (UIWindowScene *)scene; + + for (UIWindow *window in windowScene.windows) { + if (window.isKeyWindow) { + return window; + } } } + return nil; } diff --git a/packages/react-native/React/CoreModules/RCTAlertController.m b/packages/react-native/React/CoreModules/RCTAlertController.m index d7b1b9da52f323..30a41c6f246369 100644 --- a/packages/react-native/React/CoreModules/RCTAlertController.m +++ b/packages/react-native/React/CoreModules/RCTAlertController.m @@ -23,17 +23,7 @@ @implementation RCTAlertController - (UIWindow *)alertWindow { if (_alertWindow == nil) { - _alertWindow = [self getUIWindowFromScene]; - - if (_alertWindow == nil) { - UIWindow *keyWindow = RCTSharedApplication().keyWindow; - if (keyWindow) { - _alertWindow = [[UIWindow alloc] initWithFrame:keyWindow.bounds]; - } else { - // keyWindow is nil, so we cannot create and initialize _alertWindow - NSLog(@"Unable to create alert window: keyWindow is nil"); - } - } + _alertWindow = [[UIWindow alloc] initWithWindowScene:RCTKeyWindow().windowScene]; if (_alertWindow) { _alertWindow.rootViewController = [UIViewController new]; @@ -78,19 +68,6 @@ - (void)hide _alertWindow = nil; } - -- (UIWindow *)getUIWindowFromScene -{ - if (@available(iOS 13.0, *)) { - for (UIScene *scene in RCTSharedApplication().connectedScenes) { - if (scene.activationState == UISceneActivationStateForegroundActive && - [scene isKindOfClass:[UIWindowScene class]]) { - return [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; - } - } - } - return nil; -} #endif // [macOS] @end diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 30d2d56862c0a5..b278913ebf289a 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -139,12 +139,14 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo dispatch_async(dispatch_get_main_queue(), ^{ self->_showDate = [NSDate date]; + if (!self->_window && !RCTRunningInTestEnvironment()) { #if !TARGET_OS_OSX // [macOS] - UIWindow *window = RCTSharedApplication().keyWindow; + UIWindow *window = RCTKeyWindow(); CGFloat windowWidth = window.bounds.size.width; - self->_window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, windowWidth, window.safeAreaInsets.top + 10)]; + self->_window = [[UIWindow alloc] initWithWindowScene:window.windowScene]; + self->_window.frame = CGRectMake(0, 0, windowWidth, window.safeAreaInsets.top + 10); self->_label = [[UILabel alloc] initWithFrame:CGRectMake(0, window.safeAreaInsets.top - 10, windowWidth, 20)]; [self->_window addSubview:self->_label]; @@ -184,17 +186,11 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo #else // [macOS self->_label.stringValue = message; self->_label.textColor = color; + self->_label.backgroundColor = backgroundColor; [self->_window orderFront:nil]; #endif // macOS] -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ - __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 - if (@available(iOS 13.0, *)) { - UIWindowScene *scene = (UIWindowScene *)RCTSharedApplication().connectedScenes.anyObject; - self->_window.windowScene = scene; - } -#endif }); }