Send Rich Notifications

Rich notifications include images, videos, titles, and subtitles from Marketing Cloud Next and mutable content. Mutable content can include personalization in the title, subtitle, or body of your message.

Prerequisites 

  • Make sure that your app is built for iOS version 10 or later.
  • Include a service extension for your app that can handle mutable content. See Apple Developer Documentation: Modifying and Presenting Notifications.
  • Make sure that your app is registered to send push notifications in Marketing Cloud Next.

Create a Notification Service Extension 

Skip these steps if you’ve already integrated Notification Service Extension when you integrated the Mobile App Messaging SDK with your app.

  1. 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 not, add it.

The Notification Target must be signed with the same Xcode Managed Profile as the main project.

Important

This service extension checks for a _mediaUrl element in request.content.userInfo. If found, the extension attempts to download the media from the URL, creates a thumbnail-size 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. This code example assumes your app is using the Mobile App Messaging SDK without the Extension SDK integration.

Without Extension SDK Integration
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 will be used.
21        // We took too long to download the media URL. Use the alternate text if provided.
22        self.useAltText()
23
24        // Tell the OS that the process is complete and the content is ready to be presented.
25        self.contentHandler!(self.bestAttemptContent!)
26    }
27
28    private func useAltText() {
29        if let mediaAltText = self.bestAttemptContent?.userInfo["_mediaAlt"] as? String {
30            // alternative text to display if there are any issues loading the media URL
31            if mediaAltText.isEmpty == false {
32                self.bestAttemptContent?.body = mediaAltText
33            }
34        }
35    }
36
37    private func addMedia(_ request: UNNotificationRequest, completion: @escaping () -> Void) {
38        guard let mediaUrlString = request.content.userInfo["_mediaUrl"] as? String,
39                !mediaUrlString.isEmpty  else {
40            useAltText()
41            completion()
42            return
43        }
44
45        guard let mediaUrl = URL(string: mediaUrlString) else {
46            useAltText()
47            completion()
48            return
49        }
50
51        let session = URLSession(configuration: URLSessionConfiguration.default)
52        let downloadTask = session.downloadTask(with: mediaUrl) { [weak self]
53            location, response, error in
54            if let _ = error {
55                self?.useAltText()
56                completion()
57                return
58            }
59
60            guard let theLocation = location else {
61                self?.useAltText()
62                completion()
63                return
64            }
65
66            guard let theResponse = response as? HTTPURLResponse else {
67                self?.useAltText()
68                completion()
69                return
70            }
71
72            let statusCode = theResponse.statusCode
73            guard (statusCode >= 200 && statusCode <= 299) else {
74                self?.useAltText()
75                completion()
76                return
77            }
78
79            let localMediaUrl = URL.init(fileURLWithPath: theLocation.path + mediaUrl.lastPathComponent)
80
81            // Remove any existing file with the same name
82            try? FileManager.default.removeItem(at: localMediaUrl)
83
84            do {
85                try FileManager.default.moveItem(at: theLocation, to: localMediaUrl)
86            } catch {
87                self?.useAltText()
88                completion()
89                return
90            }
91
92            guard let mediaAttachment = try? UNNotificationAttachment(identifier: "SomeAttachmentId",
93                                                                      url: localMediaUrl) else {
94                self?.useAltText()
95                completion()
96                return;
97            }
98
99            guard let theContent = self?.bestAttemptContent else {
100                self?.useAltText()
101                completion()
102                return
103            }
104
105            theContent.attachments = [mediaAttachment]
106            completion()
107        }
108
109        downloadTask.resume()
110    }
111}

If your app uses the Mobile App Messaging SDK with the Extension SDK integration, use this code example instead.

With Extension SDK Integration
1//  NotificationService.swift
2//  MyServiceExtension
3
4import UserNotifications
5import MCExtensionSDK
6
7class NotificationService: SFMCNotificationService {
8
9    // Use this method to enable logging, change logging levels, etc. , if required.
10    override func sfmcProvideConfig() -> SFNotificationServiceConfig {
11        var logLevel: LogLevel = .none
12#if DEBUG
13        logLevel = .debug
14#endif
15        return SFNotificationServiceConfig(logLevel: logLevel)
16    }
17
18    // Use this method only if you need to perform any custom processing for
19    // images, video downloads, inserting custom keys in notification userInfo, etc.
20    // Don’t modify mutableContent.request.content.userInfo directly. To add any
21    // custom key in notification userInfo, return a dictionary in the
22    // completion handler.
23
24
25    // Like the `UNNotificationServiceExtension` method - func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) → Void), you'll only have limited time from the system for processing.
26    override func sfmcDidReceive(_ request: UNNotificationRequest, mutableContent: UNMutableNotificationContent, withContentHandler contentHandler: @escaping ([AnyHashable : Any]?) -> Void) {
27        // Your custom code here
28        //...
29        self.addMedia(mutableContent) {
30            // To add any custom key:value pair(s) in notifications userInfo object, then
31            var customUserInfo: [AnyHashable : Any] = [:]
32            customUserInfo["MyCustomKey"] = "MyCustomValue"
33
34            // Finally, call the content handler to signal the end of your processing operation.
35            //
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}