| 1 | diff --git a/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift b/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift |
| 2 | --- a/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift |
| 3 | +++ b/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift |
| 4 | @@ -42,8 +42,10 @@ |
| 5 | @objc |
| 6 | public static let shared = NotificationCenterManager() |
| 7 | |
| 8 | - var delegates: [NotificationDelegate] = [] |
| 9 | - var pendingResponses: [UNNotificationResponse] = [] |
| 10 | + // Module creation and destruction can overlap across React runtimes. |
| 11 | + private let stateLock = NSLock() |
| 12 | + private var delegates: [NotificationDelegate] = [] |
| 13 | + private var pendingResponses: [UNNotificationResponse] = [] |
| 14 | let userNotificationCenter: UNUserNotificationCenter = UNUserNotificationCenter.current() |
| 15 | |
| 16 | private override init() { |
| 17 | @@ -62,32 +64,49 @@ |
| 18 | } |
| 19 | |
| 20 | public func addDelegate(_ delegate: NotificationDelegate) { |
| 21 | - delegates.append(delegate) |
| 22 | + let responses = stateLock.withLock { |
| 23 | + delegates.append(delegate) |
| 24 | + return pendingResponses |
| 25 | + } |
| 26 | var handled = false |
| 27 | - for pendingResponse in pendingResponses { |
| 28 | + for pendingResponse in responses { |
| 29 | handled = delegate.didReceive(pendingResponse, completionHandler: {}) || handled |
| 30 | } |
| 31 | if handled { |
| 32 | - pendingResponses.removeAll() |
| 33 | + removePendingResponses(responses) |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | public func removeDelegate(_ delegate: AnyObject) { |
| 38 | - if let index = delegates.firstIndex(where: { $0 === delegate }) { |
| 39 | - delegates.remove(at: index) |
| 40 | + stateLock.withLock { |
| 41 | + if let index = delegates.firstIndex(where: { $0 === delegate }) { |
| 42 | + delegates.remove(at: index) |
| 43 | + } |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + private func snapshotDelegates() -> [NotificationDelegate] { |
| 48 | + stateLock.withLock { delegates } |
| 49 | + } |
| 50 | + |
| 51 | + private func removePendingResponses(_ responses: [UNNotificationResponse]) { |
| 52 | + let identifiers = Set(responses.map(ObjectIdentifier.init)) |
| 53 | + stateLock.withLock { |
| 54 | + // Keep responses received while delegate callbacks were running. |
| 55 | + pendingResponses.removeAll { identifiers.contains(ObjectIdentifier($0)) } |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // MARK: - Called by PushTokenAppDelegateSubscriber |
| 60 | |
| 61 | public func didFailRegistration(_ error: any Error) { |
| 62 | - for delegate in delegates { |
| 63 | + for delegate in snapshotDelegates() { |
| 64 | delegate.didFailRegistration(error) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | public func didRegister(_ deviceToken: String) { |
| 69 | - for delegate in delegates { |
| 70 | + for delegate in snapshotDelegates() { |
| 71 | delegate.didRegister(deviceToken) |
| 72 | } |
| 73 | } |
| 74 | @@ -100,7 +119,7 @@ |
| 75 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void |
| 76 | ) { |
| 77 | var handled = false |
| 78 | - for delegate in delegates { |
| 79 | + for delegate in snapshotDelegates() { |
| 80 | handled = delegate.willPresent(notification, completionHandler: completionHandler) || handled |
| 81 | } |
| 82 | if !handled { |
| 83 | @@ -113,18 +132,23 @@ |
| 84 | didReceive response: UNNotificationResponse, |
| 85 | withCompletionHandler completionHandler: @escaping () -> Void |
| 86 | ) { |
| 87 | + let currentDelegates = stateLock.withLock { |
| 88 | + // A delegate registering during delivery must see this response too. |
| 89 | + pendingResponses.append(response) |
| 90 | + return delegates |
| 91 | + } |
| 92 | var handled = false |
| 93 | - for delegate in delegates { |
| 94 | + for delegate in currentDelegates { |
| 95 | handled = delegate.didReceive(response, completionHandler: completionHandler) || handled |
| 96 | } |
| 97 | - if !handled { |
| 98 | - pendingResponses.append(response) |
| 99 | + if handled { |
| 100 | + removePendingResponses([response]) |
| 101 | } |
| 102 | completionHandler() |
| 103 | } |
| 104 | |
| 105 | public func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { |
| 106 | - for delegate in delegates { |
| 107 | + for delegate in snapshotDelegates() { |
| 108 | delegate.openSettings(notification) |
| 109 | } |
| 110 | } |
| 111 | @@ -132,7 +156,7 @@ |
| 112 | // MARK: - Called from NotificationsAppDelegateSubscriber |
| 113 | public func didReceive(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { |
| 114 | var handled = false |
| 115 | - for delegate in delegates { |
| 116 | + for delegate in snapshotDelegates() { |
| 117 | handled = delegate.didReceive(userInfo, completionHandler: completionHandler) || handled |
| 118 | } |
| 119 | if !handled { |