1diff --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 {