Skip to content

Commit

Permalink
[expo-notifications][iOS] Swift conversion 3: Scheduling, Notificatio…
Browse files Browse the repository at this point in the history
…nBuilder, NotificationCenterDelegate, Presentation (expo#33253)

# Why

Migrate scheduler, notification builder, and presentation module to Swift

# How

- Rewrite scheduler module in Swift
- Rewrite presentation module in Swift
- Create singleton to receive notification center events and allow modules to register as delegates for those events
- Replace Objective C dictionary categories with a Swift implementation using Records

# Test Plan

- CI should pass, including SwiftLint checks
Behavior of notification test app on real device should not change

# Checklist

- [x] Documentation is up to date to reflect these changes (eg: https://docs.expo.dev and README.md).
- [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin).
  • Loading branch information
douglowder authored Dec 13, 2024
1 parent 3059147 commit a729e1a
Show file tree
Hide file tree
Showing 18 changed files with 699 additions and 660 deletions.
1 change: 1 addition & 0 deletions packages/expo-notifications/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

- [iOS] Swift conversion 1: badge and server registration. ([#32069](https://github.com/expo/expo/pull/32069) by [@douglowder](https://github.com/douglowder))
- [iOS] Swift conversion 2: push token module. ([#32612](https://github.com/expo/expo/pull/32612) by [@douglowder](https://github.com/douglowder))
- [iOS] Swift conversion 3: scheduling, notification builder. ([#33253](https://github.com/expo/expo/pull/33253) by [@douglowder](https://github.com/douglowder))

## 0.29.11 - 2024-12-05

Expand Down
2 changes: 1 addition & 1 deletion packages/expo-notifications/expo-module.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "expo-notifications",
"platforms": ["ios", "android"],
"ios": {
"modules": ["BadgeModule", "ServerRegistrationModule", "PushTokenModule"],
"modules": ["BadgeModule", "ServerRegistrationModule", "PushTokenModule", "SchedulerModule", "PresentationModule"],
"appDelegateSubscribers": ["PushTokenAppDelegateSubscriber"]
},
"android": {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright © 2024 650 Industries. All rights reserved.

import ExpoModulesCore

struct NotificationRequestRecord: Record {
@Field
var title: String?
@Field
var subtitle: String?
@Field
var body: String?
@Field
var launchImageName: String?
@Field
var badge: Int?
@Field
var userInfo: [String: Any]?
@Field
var categoryIdentifier: String?
@Field
var sound: Either<Bool, String>?
@Field
var attachments: [[String: Any]]?
@Field
var interruptionLevel: String?
}

public class NotificationBuilder: NSObject {
public class func content(_ request: [String: Any], appContext: AppContext) throws -> UNMutableNotificationContent {
let content = UNMutableNotificationContent()
let request = try NotificationRequestRecord(from: request, appContext: appContext)

if let title = request.title {
content.title = title
}

if let subtitle = request.subtitle {
content.subtitle = subtitle
}

if let body = request.body {
content.body = body
}

if let launchImageName = request.launchImageName {
content.launchImageName = launchImageName
}

if let badge = request.badge {
// swiftlint:disable:next legacy_objc_type
content.badge = NSNumber.init(value: badge)
}

if let userInfo = request.userInfo {
content.userInfo = userInfo
}

if let categoryIdentifier = request.categoryIdentifier {
content.categoryIdentifier = categoryIdentifier
}

if let sound = request.sound {
if let soundBool = try? sound.as(Bool.self) {
content.sound = soundBool ? .default : .none
} else if let soundName = try? sound.as(String.self) {
if soundName == "default" {
content.sound = UNNotificationSound.default
} else if soundName == "defaultCritical" {
content.sound = UNNotificationSound.defaultCritical
} else {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName))
}
}
}

var attachments: [UNNotificationAttachment] = []
if let attachmentsArray = request.attachments {
for attachmentObject in attachmentsArray {
if let attachment: UNNotificationAttachment = attachment(attachmentObject) {
attachments.append(attachment)
}
}
}
content.attachments = attachments
if let interruptionLevel = request.interruptionLevel {
content.interruptionLevel = deserializeInterruptionLevel(interruptionLevel)
}

return content
}

class func attachment(_ request: [String: Any]) -> UNNotificationAttachment? {
let identifier = request["identifier"] as? String ?? ""
let uri = request["uri"] as? String ?? ""
do {
if let url = URL(string: uri),
let attachment: UNNotificationAttachment =
try? UNNotificationAttachment(
identifier: identifier,
url: url,
options: attachmentOptions(request)
) {
return attachment
}
return nil
}
}

class func attachmentOptions(_ request: [String: Any]) -> [String: Any] {
var options: [String: Any] = [:]
if let typeHint = request["typeHint"] as? String {
options[UNNotificationAttachmentOptionsTypeHintKey] = typeHint
}
if let hideThumbnail = request["hideThumbnail"] as? Bool {
options[UNNotificationAttachmentOptionsThumbnailHiddenKey] = hideThumbnail
}
if let thumbnailClipArea = request["thumbnailClipArea"] as? [String: Any] {
let x = thumbnailClipArea["x"] as? Double
let y = thumbnailClipArea["y"] as? Double
let width = thumbnailClipArea["width"] as? Double
let height = thumbnailClipArea["height"] as? Double
if let x, let y, let width, let height {
options[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
CGRect(
x: x,
y: y,
width: width,
height: height
)
}
}
if let thumbnailTime = request["thumbnailTime"] as? TimeInterval {
options[UNNotificationAttachmentOptionsThumbnailTimeKey] = thumbnailTime
}
return options
}

class func deserializeInterruptionLevel(_ interruptionLevel: String) -> UNNotificationInterruptionLevel {
switch interruptionLevel {
case "passive": return .passive
case "active": return .active
case "timeSensitive": return .timeSensitive
case "critical": return .critical
default: return .passive
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions;
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;
/*
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(nullable UNNotification *)notification;

*/
@property (nonatomic, strong, nullable) UNNotificationResponse *lastNotificationResponse;

@end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
#import <ExpoModulesCore/EXDefines.h>
#import <EXNotifications/EXNotificationsDelegate.h>

#if __has_include(<ExpoModulesCore/ExpoModulesCore-Swift.h>)
#import <EXNotifications/EXNotifications-Swift.h>
#else
#import "EXNotifications-Swift.h"
#endif

@interface EXNotificationCenterDelegate ()

@property (nonatomic, strong) NSPointerArray *delegates;
@property (nonatomic, strong) NSMutableArray<UNNotificationResponse *> *pendingNotificationResponses;
@property (nonatomic, weak) EXNotificationCenterManager *notificationCenterManager;

@end

Expand All @@ -20,6 +27,7 @@ - (instancetype)init
if (self = [super init]) {
_delegates = [NSPointerArray weakObjectsPointerArray];
_pendingNotificationResponses = [NSMutableArray array];
_notificationCenterManager = [EXNotificationCenterManager shared];
}
return self;
}
Expand Down Expand Up @@ -90,35 +98,8 @@ __block void (^completionHandlerCaller)(void) = ^{

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
__block int delegatesCalled = 0;
__block int delegatesCompleted = 0;
__block BOOL delegatingCompleted = NO;
__block UNNotificationPresentationOptions optionsSum = UNNotificationPresentationOptionNone;
__block void (^completionHandlerCaller)(void) = ^{
if (delegatingCompleted && delegatesCompleted == delegatesCalled) {
completionHandler(optionsSum);
}
};

for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
[pointer userNotificationCenter:center willPresentNotification:notification withCompletionHandler:^(UNNotificationPresentationOptions options) {
@synchronized (self) {
delegatesCompleted += 1;
optionsSum = optionsSum | options;
completionHandlerCaller();
}
}];
@synchronized (self) {
delegatesCalled += 1;
}
}
}
@synchronized (self) {
delegatingCompleted = YES;
completionHandlerCaller();
}
// Delegate to the new Swift code
[_notificationCenterManager userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
Expand Down Expand Up @@ -169,12 +150,8 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNoti

- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification
{
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]) {
[pointer userNotificationCenter:center openSettingsForNotification:notification];
}
}
// Delegate to the new Swift manager
[_notificationCenterManager userNotificationCenter:center openSettingsForNotification:notification];
}

# pragma mark - EXNotificationCenterDelegate
Expand Down

This file was deleted.

Loading

0 comments on commit a729e1a

Please sign in to comment.