Send Rich Notifications

Include images, videos, titles, and subtitles in your push notifications to create more engaging messages. Rich notifications use mutable content that can include personalization in the title, subtitle, or body of your message.

To use rich notifications, your app requires iOS 10 or later, a Notification Service Extension that handles mutable content, and registration for push notifications via the Salesforce Engagement SDK. The service extension downloads media from a URL, creates attachments, and handles fallback text if media fails to load.

Skip these steps if you’ve already integrated the Notification Service Extension.

  1. In Xcode, click File > New > Target.
  2. Select Notification Service Extension.
  3. Name and save the new extension.
  4. In your project target’s General settings, confirm that the new extension is listed in the Frameworks, Libraries, and Embedded Content section. If it isn’t, add it.

Sign the Notification Target with the same Xcode Managed Profile as the main project.

Important

This service extension checks for a _mediaUrl element in request.content.userInfo. If the extension finds this element, it tries to download the media from the URL, creates a thumbnail version, and then adds the attachment. The service extension also checks for a _mediaAlt element in request.content.userInfo. If found, the service extension uses the element for the body text if there are any problems downloading or creating the media attachment.

A service extension can timeout if it’s unable to download. In this code sample, the service extension delivers the original content with the body text changed to the value in _mediaAlt.

1import UserNotifications
2import CoreGraphics
3
4class MyNotificationService: UNNotificationServiceExtension {
5
6    var contentHandler: ((UNNotificationContent) -> Void)?
7    var bestAttemptContent: UNMutableNotificationContent?
8
9    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
10        self.contentHandler = contentHandler
11        self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
12
13        self.addMedia(request) {
14            self.contentHandler!(self.bestAttemptContent!)
15        }
16    }
17
18    override func serviceExtensionTimeWillExpire() {
19        // Called just before the extension is terminated by the system.
20        // Use this as an opportunity to deliver your "best attempt" at modified content. Otherwise, the original push payload is used.
21
22        // We took too long to download the media URL. Use the alternate text if provided.
23        self.useAltText()
24
25        // Tell the OS that the process is complete and the content is ready to be presented.
26        self.contentHandler!(self.bestAttemptContent!)
27    }
28
29    private func useAltText() {
30        if let mediaAltText = self.bestAttemptContent?.userInfo["_mediaAlt"] as? String {
31            // alternative text to display if there are any issues loading the media URL
32            if mediaAltText.isEmpty == false {
33                self.bestAttemptContent?.body = mediaAltText
34            }
35        }
36    }
37
38    private func addMedia(_ request: UNNotificationRequest, completion: @escaping () -> Void) {
39        guard let mediaUrlString = request.content.userInfo["_mediaUrl"] as? String,
40                !mediaUrlString.isEmpty  else {
41            useAltText()
42            completion()
43            return
44        }
45
46        guard let mediaUrl = URL(string: mediaUrlString) else {
47            useAltText()
48            completion()
49            return
50        }
51
52        let session = URLSession(configuration: URLSessionConfiguration.default)
53        let downloadTask = session.downloadTask(with: mediaUrl) { [weak self]
54            location, response, error in
55            if let _ = error {
56                self?.useAltText()
57                completion()
58                return
59            }
60
61            guard let theLocation = location else {
62                self?.useAltText()
63                completion()
64                return
65            }
66
67            guard let theResponse = response as? HTTPURLResponse else {
68                self?.useAltText()
69                completion()
70                return
71            }
72
73            let statusCode = theResponse.statusCode
74            guard (statusCode >= 200 && statusCode <= 299) else {
75                self?.useAltText()
76                completion()
77                return
78            }
79
80            let localMediaUrl = URL.init(fileURLWithPath: theLocation.path + mediaUrl.lastPathComponent)
81
82            // Remove any existing file with the same name
83            try? FileManager.default.removeItem(at: localMediaUrl)
84
85            do {
86                try FileManager.default.moveItem(at: theLocation, to: localMediaUrl)
87            } catch {
88                self?.useAltText()
89                completion()
90                return
91            }
92
93            guard let mediaAttachment = try? UNNotificationAttachment(identifier: "SomeAttachmentId",
94                                                                      url: localMediaUrl) else {
95                self?.useAltText()
96                completion()
97                return;
98            }
99
100            guard let theContent = self?.bestAttemptContent else {
101                self?.useAltText()
102                completion()
103                return
104            }
105
106            theContent.attachments = [mediaAttachment]
107            completion()
108        }
109
110        downloadTask.resume()
111    }
112}
1//
2//  NotificationService.swift
3//  MyServiceExtension
4//
5//
6
7import UserNotifications
8import MCExtensionSDK
9
10class NotificationService: SFMCNotificationService {
11
12    // Use this method to enable logging, change logging levels, etc. , if required.
13    override func sfmcProvideConfig() -> SFNotificationServiceConfig {
14        var logLevel: LogLevel = .none
15#if DEBUG
16        logLevel = .debug
17#endif
18        return SFNotificationServiceConfig(logLevel: logLevel)
19    }
20
21    // To perform custom processing for images, video downloads, or inserting custom keys in notification userInfo, use this method.
22    // Don’t modify `mutableContent.request.content.userInfo` directly. Doing so can trigger an exception.
23    // To add a custom key in notification userInfo, return a dictionary in the completion handler.
24
25
26    // Like the `UNNotificationServiceExtension` method - func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) → Void), the system allows only a limited time for processing.
27    override func sfmcDidReceive(_ request: UNNotificationRequest, mutableContent: UNMutableNotificationContent, withContentHandler contentHandler: @escaping ([AnyHashable : Any]?) -> Void) {
28        // Your custom code here
29        //...
30        self.addMedia(mutableContent) {
31            // Add custom key:value pairs in the userInfo object
32            var customUserInfo: [AnyHashable : Any] = [:]
33            customUserInfo["MyCustomKey"] = "MyCustomValue"
34
35            // Finally, call the content handler to signal the end of your processing operation.
36            contentHandler(customUserInfo)
37        }
38    }
39
40    private func addMedia(_ mutableContent: UNMutableNotificationContent, completion: @escaping () -> Void) {
41        guard let mediaUrlString = mutableContent.userInfo["_mediaUrl"] as? String,
42                !mediaUrlString.isEmpty  else {
43            completion()
44            return
45        }
46
47        guard let mediaUrl = URL(string: mediaUrlString) else {
48            completion()
49            return
50        }
51
52        let session = URLSession(configuration: URLSessionConfiguration.default)
53        let downloadTask = session.downloadTask(with: mediaUrl) { [weak self]
54            location, response, error in
55            if let _ = error {
56                completion()
57                return
58            }
59
60            guard let theLocation = location else {
61                completion()
62                return
63            }
64
65            guard let theResponse = response as? HTTPURLResponse else {
66                completion()
67                return
68            }
69
70            let statusCode = theResponse.statusCode
71            guard (statusCode >= 200 && statusCode <= 299) else {
72                completion()
73                return
74            }
75
76            let localMediaUrl = URL.init(fileURLWithPath: theLocation.path + mediaUrl.lastPathComponent)
77
78            // Remove any existing file with the same name
79            try? FileManager.default.removeItem(at: localMediaUrl)
80
81            do {
82                try FileManager.default.moveItem(at: theLocation, to: localMediaUrl)
83            } catch {
84                completion()
85                return
86            }
87
88            guard let mediaAttachment = try? UNNotificationAttachment(identifier: "SomeAttachmentId",
89                                                                      url: localMediaUrl) else {
90                completion()
91                return;
92            }
93
94            mutableContent.attachments = [mediaAttachment]
95            completion()
96        }
97
98        downloadTask.resume()
99    }
100}