Skip to main content
SDKsiOS

Rich notification images

Notification Service Extension, payload contract, and MotiSigRichPushHandler on iOS.

iOS does not download remote image URLs by itself. To show an image inside the notification banner, your app must:

  1. Send the push with mutable-content: 1 (the MotiSig backend sets this automatically when an image URL is present).
  2. Embed a Notification Service Extension (NSE) that downloads the URL and attaches it as a UNNotificationAttachment before iOS renders the banner.

The MotiSig Swift package ships MotiSigRichPushHandler so the NSE is a one-line forwarder. The same product is linked by both your app target and the NSE target.

Expected APNs payload

{
  "aps": {
    "alert": { "title": "...", "body": "..." },
    "mutable-content": 1,
    "sound": "default"
  },
  "messageId": "f082aa55-6eed-407f-b819-36e858ed7d0a",
  "_motisig": {
    "imageUrl": "https://your-cdn.example.com/path/push.jpg"
  }
}

Image URLs must be HTTPS and reachable from the NSE process — no auth headers, no signed cookies that depend on the host app. If your CDN requires auth, sign the URL itself (query-string token).

Image URL resolution order

MotiSigRichPushHandler.extractImageURLString(from:) returns the first non-empty match from:

  1. _motisig.imageUrl / _motisig.image_url / _motisig.image (MotiSig canonical)
  2. _richContent.image (Expo push relay)
  3. fcm_options.image (FCM relay)
  4. Top-level image / imageUrl / image_url (host-app convenience)

This order matches the Android and Expo helpers so you can author one payload that works across MotiSig client SDKs.

Add the NSE target (Xcode, one-time)

  1. File → New → Target… → Notification Service Extension. Name it whatever you want (for example, MotiSigNSE).
  2. Set the extension's bundle identifier to a child of the host app's bundle id (for example, com.your.app.MotiSigNSE). iOS rejects embedded extensions whose bundle id is not a prefix of the host's.
  3. With the extension target selected → General → Frameworks and Libraries → +, add the MotiSig Swift package product. The host app and the NSE link MotiSig independently; both bindings are normal.
  4. Replace the auto-generated NotificationService.swift with:
import MotiSig
import UserNotifications

class NotificationService: UNNotificationServiceExtension {
    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        MotiSigRichPushHandler.handle(request: request, contentHandler: contentHandler)
    }
}
  1. Build and run the host scheme. Confirm the extension is embedded by checking that YourApp.app/PlugIns/MotiSigNSE.appex exists in the build products.

A working reference target lives in examples/motisig-ios-example/MotiSigNSE — open motisig-ios-example.xcodeproj in that example folder for a complete buildable sample.

Foreground banners

When the app is in the foreground, iOS does not show a banner unless the UNUserNotificationCenterDelegate returns banner presentation options. The MotiSig SDK installs its own delegate proxy and returns [.banner, .list, .sound] by default, so foreground banners — including the image attached by the NSE — appear without extra setup.

If your app installs its own UNUserNotificationCenterDelegate, make sure your willPresent returns options that include .banner (or .alert on older iOS), otherwise the foreground banner is suppressed even though the NSE attached the image correctly.

Tap handling

The image attachment is a presentation detail; tap callbacks (MotiSigNotificationListener.motiSig(didReceiveNotification:inForeground:)) and click tracking are unchanged. The same userInfo payload that fed the NSE is delivered to listeners, so you can still read _motisig.imageUrl (or any custom field) from notification.userInfo to render an in-app detail view.

Troubleshooting

SymptomWhat to check
NSE never invokedaps.mutable-content == 1 in the delivered payload. Without it, iOS never hands the request to the extension.
NSE runs, no imageURL present under one of the resolved keys above; URL is HTTPS; URL is publicly reachable from the device's network.
Foreground banner missing imageYour app overrides the UNUserNotificationCenterDelegate and returns no banner options from willPresent.
Embedded-binary id errorThe NSE bundle id is not a prefix-child of the host (for example, host com.your.app requires NSE id like com.your.app.MotiSigNSE).
Silent download failureApp Transport Security blocks plain http:// and self-signed certs. Open Console.app → filter by the NSE bundle id to inspect logs from inside the extension.

For a deeper troubleshooting checklist covering the rest of the SDK, see Troubleshooting.

Expo apps

Do not add the NSE target manually for a managed Expo workflow. Follow Rich notification images for the Expo SDK and the expo-motisig-sdk example.