I'm developing an app that receives push notifications, and writes the contents of the push notification to a shared location between the main app and a Notifications Message Extension, through App Groups. This all seems to work on my phone, with developer mode turned on, but when I archive my app as an Enterprise IPA and distribute it, the users can install the app on their phones and they receive the push notifications, but it doesn't appear that the message extension is running as my app displays the content of the shared data in the App Groups on the main screen and nothing is showing. I have tried on 3 phones, and it only works on the phone with developer mode turned on. I can't tell at this point whether it's because of a signing issue, or build phase order issue, or something else?
My Notifications Message Extension doesn't seem to run after distributing my app via Enterprise IPA
There are indeed many reasons the extension may seem to not run, and you will need to debug this. Among the many causes, the reasons the NSE fails to modify the message could be:
- it crashes at launch
- it crashes while modifying or exits early without setting the content and calling contentHandler()
- it goes over the memory and time limits and is terminated by the system
- it is trying to access on-disk objects that are not available (perhaps due to security settings) and fails
- the reentrancy of the NSE causes problems if any global variables/singletons/shared objects are not designed accordingly that the same NSE process could be used for multiple notifications
- it is unable to be started by the system for some reason (the Console log will show those issues)
- the extension is not configured or built correctly in your application bundle
If you narrow down the issue, we would be able to give some more useful pointers.
Since it works on one of my devices (with developer mode enabled) but not the other (with just trusting the app), are you able to give me any things to try to help narrow those things down? It's very difficult to troubleshoot on the device that it's not working on when you can't see where it's failing , at any of those stages you mention above?
Here is the code of my message extension, if that helps. I am indeed trying to write to the shared storage of the App Groups
import os.log // Apple's modern, fast, privacy-safe logging system
class NotificationService: UNNotificationServiceExtension {
private let log = OSLog(
subsystem: Bundle.main.bundleIdentifier!,
category: "pushnotificationsmessageextension"
)
var contentHandler: ((UNNotificationContent) -> Void)!
// A mutable copy of the notification content — this is what we'll modify or save
var bestAttemptContent: UNMutableNotificationContent?
// Main entry point — called every time a push arrives with `mutable-content: 1`
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler:
@escaping (UNNotificationContent) -> Void
) {
// Save the handler so we can call it later (required!)
self.contentHandler = contentHandler
// Make a mutable copy so we can modify title, body, attachments, etc.
bestAttemptContent =
request.content.mutableCopy() as? UNMutableNotificationContent
// If something went wrong making the mutable copy, just pass through the original
guard let bestAttemptContent = bestAttemptContent else {
contentHandler(request.content)
return
}
// 1. Create a mutable dictionary to work with
var mutablePayload = request.content.userInfo
// extract Title and Body from the 'aps' dictionary
let aps = mutablePayload["aps"] as? [String: Any]
let alert = aps?["alert"] as? [String: Any]
let title = alert?["title"] as? String ?? "No Title"
let body = alert?["body"] as? String ?? "No Body"
// 2. Add the current timestamp as an ISO 8601 string
let now = Date()
let formatter = ISO8601DateFormatter()
// Set the timezone to America/Toronto (covers both Toronto and New York Eastern Time)
if let easternTimeZone = TimeZone(identifier: "America/Toronto") {
formatter.timeZone = easternTimeZone
} else {
// Fallback: If the specified timezone isn't found, use UTC (Good for reliability)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
print(
"WARNING: 'America/Toronto' TimeZone not found, falling back to UTC for timestamp."
)
}
// Set the format to include the time and timezone offset (e.g., 2025-12-08T11:34:21-05:00)
formatter.formatOptions = [
.withInternetDateTime, .withFractionalSeconds, .withTimeZone,
]
let timestampString = formatter.string(from: now)
let uuid = UUID()
let minimalPayload: [String: Any] = [
"unread": true,
"timestamp": timestampString,
"title": title,
"body": body,
"uuid": uuid.uuidString
]
// 3. Serialize the MODIFIED dictionary into a JSON string
let jsonData = try? JSONSerialization.data(
withJSONObject: minimalPayload,
options: []
)
let newJsonString = jsonData.flatMap {
String(data: $0, encoding: .utf8)
}
// Access shared container (App Groups) between main app and extension
guard
let shared = UserDefaults(
suiteName: "group.com.mycompany.pushnotifications"
)
else {
print(
"FATAL ERROR: Could not initialize shared UserDefaults (App Group may be missing or incorrect)."
)
return
}
if let stringToSave = newJsonString {
// 4. Read the existing HISTORY string (not array)
// If the key doesn't exist, it defaults to an empty JSON array string "[]"
let existingHistoryString =
shared.string(forKey: "push_notification_history_json") ?? "[]"
// 5. Convert the existing JSON string back into a Swift array of strings
var notificationHistory: [String] = []
if let data = existingHistoryString.data(using: .utf8),
let array = try? JSONSerialization.jsonObject(
with: data,
options: []
) as? [String]
{
notificationHistory = array
}
// 6. Add the new, timestamped JSON string to the list
notificationHistory.append(stringToSave)
// Optional: Limit the size of the history to prevent the storage file from growing infinitely.
// E.g., keep only the last 100 notifications.
let maxHistoryCount = 100
if notificationHistory.count > maxHistoryCount {
// Keeps the latest 'maxHistoryCount' items
notificationHistory.removeFirst(notificationHistory.count - maxHistoryCount)
}
// 7. Serialize the ENTIRE array of JSON strings back into ONE single JSON string
if let dataToWrite = try? JSONSerialization.data(
withJSONObject: notificationHistory,
options: []
),
let finalHistoryString = String(
data: dataToWrite,
encoding: .utf8
)
{
// 8. Save the final JSON string under a new key (renamed for clarity)
shared.set(
finalHistoryString,
forKey: "push_notification_history_json"
)
shared.synchronize()
print(
"Successfully saved entire history as one JSON string. Current count: \(notificationHistory.count)"
)
} else {
print(
"FATAL ERROR: Could not re-serialize history array for saving."
)
}
} else {
print(
"WARNING: Could not serialize payload. Nothing was saved to history."
)
}
// FINALLY: tell iOS to show the notification (with our modifications if any)
contentHandler(bestAttemptContent)
}
// Called by iOS when it's about to kill the extension due to timeout (~30 seconds)
// If we haven't called contentHandler yet, we do it now with whatever we have
// Prevents notification from being dropped entirely
override func serviceExtensionTimeWillExpire() {
// iOS is about to kill the extension – deliver what we have
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent
{
contentHandler(bestAttemptContent)
}
}
}