My Notifications Message Extension doesn't seem to run after distributing my app via Enterprise IPA

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?

Answered by a344254 in 869721022

After aligning the deployment targets on both the main target and the extension, it's working properly now

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)
        }
    }
}

Plugging a non-developer phone into my Mac, with the app installed, I opened the Console.app and watched the logs as it received a push notification. I could see messages like this

error 07:39:39.072279-0500 SpringBoard [com.mycompany.pushnotifications] No service extension record found for app error 07:39:39.072396-0500 SpringBoard [com.mycompany.pushnotifications] No valid extension available for bundle error 07:39:39.072530-0500 SpringBoard [com.mycompany.pushnotifications] Error was encountered trying to find service extension: error=Error Domain=UNErrorDomain Code=1904 "Unknown application" UserInfo={NSLocalizedDescription=Unknown application}

I have verified that in Xcode, my main target "Runner" has a bundle identifier of com.mycompany.pushnotifications and my extension target has a bundle identifier of com.mycompany.pushnotifications.pushnotificationsmessageextension

I unzipped my .IPA file and see Payload/Runner.app/pushnotificationsmessageextension.appex

In my uncompiled app, I looked in the pushnotificationsmessageextension/Info.plist file and see the following

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>NSExtension</key>
        <dict>
                <key>NSExtensionPointIdentifier</key>
                <string>com.apple.usernotifications.service</string>
                <key>NSExtensionPrincipalClass</key>
                <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
        </dict>
</dict>
</plist>
Accepted Answer

After aligning the deployment targets on both the main target and the extension, it's working properly now

My Notifications Message Extension doesn't seem to run after distributing my app via Enterprise IPA
 
 
Q