Customize Push Notification Functionality for iOS Apps

Use these customization options in your app to extend push notification capabilities beyond the default settings.

Specify a Custom Notification Sound 

This feature uses a sound included in your mobile app as a custom audio signal when a push message arrives on the mobile device.

  1. Add a file named custom.caf to your project. The cloud push payload looks for a file named custom.caf. The project file explorer in Xcode, showing a file named custom.caf.
  2. Add the custom.caf audio file to the Copy Bundle Resources folder in Xcode for your app. The Build Phases tab in Xcode, showing the Copy Bundle Resources phase with the custom.caf file selected.
  3. To prevent Xcode from using cached files, perform a clean build of the folder in Xcode. In the Product menu, hold the Option key and select Clean Build Folder.

Display Interactive Notifications 

Use interactive notifications to add buttons to push notifications from your mobile app.

This code example shows how to configure the SDK to display interactive notifications.

1func configureSdk() -> Bool {
2
3  // Enable logging for debugging early on. Debug level is not recommended for
4  // production apps because a large amount of data is logged to the console.
5
6  #if DEBUG
7    SFMCSdk.setLogger(logLevel: .debug)
8  #endif
9
10  // Use the `PushFeatureConfigBuilder` to configure the Push Feature SDK. This
11  // gives you the maximum flexibility in SDK configuration. The builder lets you
12  //configure the module parameters at runtime.
13
14  let pushFeatureConfiguration = PushFeatureConfigBuilder()
15    .setApplicationControlsBadging(true)
16    .build()
17
18  // Set the completion handler to take action when all modules initialization
19  // is completed. Setting the completion handler is optional.
20
21  let completionHandler: ((_ status: [ModuleInitStatus]) -> Void) = { [weak self] status in
22    DispatchQueue.main.async {
23      self?.handleSDKInitializationCompletion(status: status)
24    }
25  }
26
27  SFMCSdk.initializeSdk(
28    ConfigBuilder().setPushFeature(config: pushFeatureConfiguration).build(),
29    completion: completionHandler)
30
31  return true
32}
33
34// MARK: - SDK Initialization Completion Handler
35
36private func handleSDKInitializationCompletion(status: [ModuleInitStatus]) {
37  var allSuccessful = true
38
39  for moduleStatus in status {
40    print(
41      "Module: \(moduleStatus.moduleName.rawValue), Status: \(moduleStatus.initStatus.rawValue)")
42
43    if moduleStatus.initStatus == .success {
44      // Handle successful initialization for each module
45      switch moduleStatus.moduleName {
46      case .pushFeature:
47        setupPushFeature()
48      default:
49        break
50      }
51    } else if moduleStatus.initStatus == .error {
52      allSuccessful = false
53      // module failed to initialize, check logs for more details
54    } else if moduleStatus.initStatus == .cancelled {
55      allSuccessful = false
56      // module initialization was cancelled
57      // (for example if re-configuration was triggered before init completed)
58    } else if moduleStatus.initStatus == .timeout {
59      allSuccessful = false
60      // module failed to initialize due to timeout, check logs for more details
61    }
62  }
63  if allSuccessful {
64    print("SDK initialization completed successfully")
65  } else {
66    print("SDK initialization completed with errors - check logs above")
67  }
68}
69
70func setupPushFeature() {
71  // Set the URLHandlingDelegate to handle URLs from CloudPage, OpenDirect,
72  // Location, and Inbox messages. In this example, the AppDelegate class adheres
73  // to the URLHandlingDelegate protocol (see below).
74  PushFeature.requestSdk { pushFeature in
75    DispatchQueue.main.async {
76      pushFeature?.setURLHandlingDelegate(self)
77    }
78  }
79
80  // Make sure to dispatch this to the main thread, as UNUserNotificationCenter
81  // will present UI.
82  DispatchQueue.main.async {
83    // Set the UNUserNotificationCenterDelegate to a class adhering to this protocol.
84    // In this example, the AppDelegate class adheres to the protocol (see below)
85    // and handles Notification Center delegate methods from iOS.
86    UNUserNotificationCenter.current().delegate = self
87
88    // Request authorization from the user for push notification alerts.
89    UNUserNotificationCenter.current().requestAuthorization(
90      options: [.alert, .sound, .badge],
91      completionHandler: { (_ granted: Bool, _ error: Error?) -> Void in
92        if error == nil {
93          if granted == true {
94            DispatchQueue.main.async {
95              UIApplication.shared.registerForRemoteNotifications()
96            }
97            // Support notification categories
98            let exampleAction = UNNotificationAction(
99              identifier: "App", title: "Example", options: [])
100            let appCategory = UNNotificationCategory(
101              identifier: "Example", actions: [exampleAction],
102              intentIdentifiers: [] as? [String] ?? [String](), options: [])
103            let categories = Set<AnyHashable>([appCategory])
104            UNUserNotificationCenter.current().setNotificationCategories(
105              categories as? Set<UNNotificationCategory> ?? Set<UNNotificationCategory>())
106          }
107        }
108      }
109    )
110  }
111}
112
113func application(
114  _ application: UIApplication,
115  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
116) -> Bool {
117  self.configureSFMCSdk()
118  return true
119}

Handle Actions 

In your push handler, examine the push notification’s payload to see if your action is triggered and if your application performed the action, as shown in this code example.

1// The method is called on the delegate when the user responds to the notification
2// by opening the application, dismissing the notification, or choosing a
3// UNNotificationAction. The delegate must be set before the application returns
4//from applicationDidFinishLaunching:.
5func userNotificationCenter(
6  _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
7  withCompletionHandler completionHandler: @escaping () -> Void
8) {
9  // tell the SDK about the notification
10  PushFeature.requestSdk { pushFeature in
11    pushFeature?.setNotificationResponse(response)
12  }
13  // Check your notification custom actions
14  if response.actionIdentifier == "App" {
15    // Handle your notification’s custom action here
16  }
17}

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}

Messages with an OpenDirect URL 

OpenDirect customized push messages contain a URL in the payload. The Mobile App Messaging SDK can pass this URL to your application to handle. For information about handling URLs from push notifications that use OpenDirect, see Handle URLs.

Handle URLs 

The SDK doesn’t automatically present URLs from these sources:

  • CloudPages URLs from push notifications
  • OpenDirect URLs from push notifications
  • CloudPages URLs from inbox messages using the built-in UITableView delegate

To handle URLs from these sources, follow these steps.

  1. Implement the URLHandlingDelegate protocol in your app.
  2. Use setURLHandlingDelegate method to set a delegate for the protocol.
  3. Next, you’re prompted to implement the protocol method sfmc_handleURL:type:

When an OpenDirect or CloudPages push notification is received, the SDK passes a NSURL value to sfmc_handleURL:type:. This value contains the push notification or inbox message, and includes the URL. A type value also reflects the source of the URL, which is either SFMCURLTypeCloudPages or SFMCURLTypeOpenDirect.

If the development language is Swift, the class that implements the URLHandlingDelegate must be compatible with Objective-C. Prefix the class with @objc and extend NSObject, as shown in this example.

Important

1// Framework import
2import SFMCSDK
3import PushFeatureSDK
4
5// Make sure your class adopts the protocol
6@objc class MyClass: NSObject, URLHandlingDelegate
7
8...
9// Set the delegate somewhere in your application code (after configuring the SDK)
10  PushFeature.requestSdk { pushFeature in
11    pushFeature?.setURLHandlingDelegate(self)
12  }
13...
14
15// EXAMPLE IMPLEMENTATIONS
16// Implement the protocol method and have iOS handle the URL itself
17func sfmc_handleURL(_ url: URL, type: String) {
18  if UIApplication.shared.canOpenURL(url) == true {
19    if #available(iOS 10.0, *) {
20      UIApplication.shared.open(url, options: [:], completionHandler: { success in
21        if success {
22          print("url \(url) opened successfully")
23        } else {
24          print("url \(url) could not be opened")
25        }
26      })
27    } else {
28      if UIApplication.shared.openURL(url) == true {
29        print("URL \(url) opened successfully")
30      } else {
31        print("Couldn't open URL \(url)")
32      }
33    }
34  }
35}
36
37// Implement the protocol method and use SFSafariViewController to present the URL within your application
38func sfmc_handleURL(_ url: URL, type: String) {
39  let safariViewController = SFSafariViewController(url: url)
40  window?.topViewController()?.present(safariViewController, animated: true) {}
41}
42
43// Implement the protocol method and take application-specific actions based on the URL itself
44func sfmc_handleURL(_ url: URL, type: String) {
45  let queryItems = URLComponents(string: url.absoluteString)?.queryItems
46  for item: URLQueryItem? in queryItems ?? [] {
47    // do something in your application based on the parameters in the URL
48  }
49}