Push Notifications Tutorial for iOS: Rich Push Notifications

Learn how to modify and enhance push notifications before they are presented to the user, how to create custom UI around your push content, and more! By Mark Struzinski.

See course reviews 4.6 (14) · 1 Review

Download materials
Save for later

Learn how to modify and enhance push notifications before they are presented to the user, how to create custom UI around your push content, and more!

If you’ve used a mobile device for the last decade, you’ve likely encountered innumerable of push notifications. Push notifications allow apps to broadcast alerts to users — even if they’re not actively using the devices.

While notifications can present helpful information to the user, the true power of notifications comes from the concept called rich notifications. Rich notifications allow you to intercept notification payloads and gives you time to dress them up in a way that best suits your user’s needs. This allows you to show custom UI that can include button actions that offer shortcuts to your users.

This tutorial assumes you have some knowledge of push notifications. If you need to brush up on the basics, check out Push Notifications Tutorial: Getting Started. That tutorial will teach you how to send and receive push notifications and utilize actions inside your push content.

This tutorial will take that knowledge further. You’ll learn how to modify and enhance incoming content, how to create custom UI around your push content and more!

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial.

Because push notifications only work on a physical device, you’ll need to configure the Apple developer portal with several properties to work through this tutorial. Xcode can handle most of this for you through automatic provisioning.

Setting up New App Values

Open the starter project in Xcode. If you haven’t signed into your development team yet, go to Preferences, select the Accounts tab and then sign in using the + button.

Sign in to the Developer Account

Next, select the Wendercast project node in the File navigator. Be sure that the Wendercast target is selected. In the middle pane, go to the Signing & Capabilities tab and check the Automatically manage signing box.

Enable automatic signing

Set the following values:

  1. Select your development team from the Team drop-down.
  2. In Bundle Identifier, enter a unique Bundle ID.
  3. Under App Groups, click the +. Leave the group prefix, and enter the same bundle ID used in the previous step.
  4. Make sure Push Notifications is present in the capabilities list.

Signing and Capabilities tab

If you see two distinct Signing (debug) and Signing (release) sections, configure Automatically manage signing, Team and Bundle Identifier on both. Use the same values.

Finally, open DiskCacheManager.swift. Then update the groupIdentifier property to your new app group ID:

let groupIdentifier = "[[group identifier here]]"

Creating an Authentication Key

For this step, log in to the Apple developer portal. Click the Account tab, and then follow these instructions:

  1. Select Certificates, ID, & Profiles from the left sidebar.
  2. From the Certificates, Identifiers, & Profiles screen, select Keys.
  3. Click the + button.
  4. Fill out the Key Name field, select Apple Push Notifications Service (APNs) and click Continue.
    Registering a new key
  5. Click Register.
  6. Make a note of your Key ID. You’ll need it for a later step.
  7. Click Download to save the .p8 file to disk.
  8. Make another note of your team ID; it’s displayed in the right corner of the page (below your name or company name).

Phew! That was a lot. Now that you’ve completed the portal work, it’s time to return to configuring the starter app.

This step will let the app read and write to the shared app container. It’s necessary when you add an extension so the app and extension both have access to the Core Data store.

Oh, and in case you’re wondering: Yes, you’ll create not one but two extensions, and the app will use Core Data. :]

Running the App

The Wendercast app fetches and parses the Ray Wenderlich podcast feed. It then displays the results in a UITableView. Users can tap on any episode to open a detail view and begin streaming the episode. They also have the ability to favorite any episode.

As mentioned, the app uses Core Data for persistence between sessions. During a feed refresh, the app only inserts new episodes.

To start, ensure your iPhone is the selected device. Then build and run. You should see a list of podcasts and an immediate prompt to enable notifications. Tap Allow.

Main podcast screen with notification prompt

Tap into any episode in the list, and you’ll see a detail screen. The selected podcast should immediately begin playing.

Podcast detail screen

Awesome! Now, you’ll send a test push notification.

Open the Xcode console. You should see the device token printed in the logs:

Permission granted: true
Notification settings:
  <UNNotificationSettings: 0x2808abaa0; authorizationStatus: Authorized,
   notificationCenterSetting: Enabled, soundSetting: Enabled,
   badgeSetting: Enabled, lockScreenSetting: Enabled,
   carPlaySetting: NotSupported, announcementSetting: NotSupported,
   criticalAlertSetting: NotSupported, alertSetting: Enabled,
   alertStyle: Banner,
   groupingSetting: Default providesAppNotificationSettings: No>
Device Token: [[device-token-here]]

Save this value because you’ll need it, well, right now.

Testing a Push Notification

Before you can test push notifications, you need to be able to send them.

Sending a push notification requires you to invoke a REST API on the Apple Push Notification Server (APNS). It’s definitely not the easiest way — especially if you have to do it manually.

Fortunately, there’s another way. Just have an app do that for you. :]

Follow these instructions:

  1. Download and install the Push Notifications Tester app.
  2. Launch the app.
    Note: If macOS complains it can’t launch the app, Right-click the app in Finder and choose Open.
  3. Select the iOS tab.
  4. Choose Token under Authentication.
  5. Click Select P8 and then select the .p8 file you saved to disk.
  6. Enter the .p8 key ID into the Enter key id field. You copied it right before downloading the p8 file.
  7. Enter your team ID in the Enter team id field.
  8. Enter your app’s bundle ID under Body.
  9. Enter the device token you saved from the previous step.
  10. Leave Collapse id blank.
  11. Leave the payload in the body as is.

Where you’re finished, it should look like this:

Testing a push notification

Send the app to the device background, but leave the device unlocked. Click Send in the Push Notifications Tester app. You’ll receive a push notification along with a success message.

Receiving a push notification

Hooray! Now that you’re all set up, it’s finally time to dive into the code.

Modifying Push Content

Apple has created a way to modify push content prior to delivery with service extensions. Service extensions allow you to intercept the push content coming in from the APNS, modify it and then deliver the modified payload to the user.

The service extension sits between the APNS server and the final content of the push notification:

Service extension diagram

Introducing Service Extensions

A service extension gets a limited execution time to perform some logic on the incoming push payload. Some of the things you can do to modify and augment the push payload are:

  • Update the title, subtitle or body of the push.
  • Add a media attachment to the push.

Adding a Service Extension

Go back to the Wendercast project and create a new target by clicking FileNewTarget….

Filter for the Notification Service Extension and click Next:

Adding notification service extension target

Name the extension WendercastNotificationService. The fields should look something like this:

Service extension configuration

Once you’ve verified the field inputs, click Finish. Do not activate the new scheme if prompted.

With that, you’ve added a notification service extension into the project and you’re ready to intercept some push notifications. :]

Exposing Files to the Extension

You’ll begin by exposing some of the helper classes that were included in the project to the new services extension you created. In the Network directory, you’ll find two files: ImageDownloader.swift and NetworkError.swift.

In the File inspector, add a check to the WendercastNotificationService target so they can be used inside the services extension:

Target membership setup

Saving Files

In the WendercastNotificationService group, open NotificationService.swift and import UIKit at the top of the file.

import UIKit

At the bottom of NotificationService, add this convenient method to save an image to disk:

private func saveImageAttachment(
  image: UIImage,
  forIdentifier identifier: String
) -> URL? {
  // 1
  let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
  // 2
  let directoryPath = tempDirectory.appendingPathComponent(
    isDirectory: true)

  do {
    // 3
    try FileManager.default.createDirectory(
      at: directoryPath,
      withIntermediateDirectories: true,
      attributes: nil)

    // 4
    let fileURL = directoryPath.appendingPathComponent(identifier)

    // 5
    guard let imageData = image.pngData() else {
      return nil

    // 6
    try imageData.write(to: fileURL)
      return fileURL
    } catch {
      return nil

Here’s what you’ve done:

  1. Obtain a reference to the temp file directory.
  2. Using the temp file directory, create a directory URL using a unique string.
  3. The FileManager is responsible for creating the actual file to store the data. Call createDirectory(at:winthIntermediateDirectories:attributes:) to create an empty directory.
  4. Create a file URL based on the image identifier.
  5. Create a Data object from the image.
  6. Attempt to write the file to disk.

Now that you’ve created a way to store the image, you’ll turn your attention to downloading the actual image.

Downloading an Image

Add another method to download an image from a URL:

private func getMediaAttachment(
  for urlString: String,
  completion: @escaping (UIImage?) -> Void
) {
  // 1
  guard let url = URL(string: urlString) else {

  // 2
  ImageDownloader.shared.downloadImage(forURL: url) { result in
    // 3
    guard let image = try? result.get() else {

    // 4

What it does is pretty simple:

  1. Ensure you can create a URL out of the urlString property.
  2. Use the ImageDownloader you linked to this target to attempt the download.
  3. Ensure the resulting image is not nil.
  4. Call the completion block, passing the UIImage result.

Modifying the Push Content From the Server

You’ll need to add a few extra values to the push payload you are sending to the device. Go into the Push Notification Tester app and replace the body with this payload:

  "aps": {
    "alert": {
      "title": "New Podcast Available",
      "subtitle": "Antonio Leiva – Clean Architecture",
      "body": "This episode we talk about Clean Architecture with Antonio Leiva."
    "mutable-content": 1
  "podcast-image": "https://koenig-media.raywenderlich.com/uploads/2016/11/Logo-250x250.png",
  "podcast-guest": "Antonio Leiva"

This payload gives your push notification a title, a subtitle and a body.

Notice mutable-content has a value of 1. This value tells iOS that the content is updatable, causing it to invoke the service extension before delivering it to the user.

There are two custom keys added: podcast-image and podcast-guest. You’ll use the values associated with these keys to update the push content before displaying the notification to the user.

Send the push with the above content now. You’ll see an updated push notification with the title, subtitle and description added. It looks like this:

Push with modified content

Updating the Title

The power of the notification service extension comes from its ability to intercept pushes. You’ll get a taste of that in this section. In WendercastNotificationService, open NotificationService.swift and locate didReceive(_:withContentHandler:). This function is called when a push notification comes in, and allows you to perform some adjustments to the content you’ll be displaying to the user.

Replace the if let block with following:

if let bestAttemptContent = bestAttemptContent {
  // 1
  if let author = bestAttemptContent.userInfo["podcast-guest"] as? String {
    // 2
    bestAttemptContent.title = "New Podcast: \(author)"

  // 3

Here’s what you’ve done:

  1. Check for a value of the key podcast-guest in userInfo in the notification content.
  2. If it exists, update the title of the notification content.
  3. Call the completion handler to deliver the push. If podcast-author‘s value is not present, the push displays the original title.

Build and run. Then send the app to the background. Now send a push from the Push Notifications Tester app. You should see a push notification with an updated title that now contains the value from the podcast-author entry.

Push with updated title

Adding an Image

Next, you’ll use the push payload to download an image representing the podcast episode.

Replace the line contentHandler(bestAttemptContent) with the following:

// 1
guard let imageURLString =
  bestAttemptContent.userInfo["podcast-image"] as? String else {

// 2
getMediaAttachment(for: imageURLString) { [weak self] image in
  // 3
    let self = self,
    let image = image,
    let fileURL = self.saveImageAttachment(
      image: image,
      forIdentifier: "attachment.png") 
    // 4
    else {

  // 5
  let imageAttachment = try? UNNotificationAttachment(
    identifier: "image",
    url: fileURL,
    options: nil)

  // 6
  if let imageAttachment = imageAttachment {
    bestAttemptContent.attachments = [imageAttachment]

  // 7

Here’s what’s happening above:

  1. Check if you have a value of podcast-image. If not, call the content handler to deliver the push and return.
  2. Call the convenience method to retrieve the image with the URL received from the push payload.
  3. When the completion block fires, check that the image is not nil; otherwise, attempt to save it to disk.
  4. If a URL is present, then the operation was successful; if any of these checks fail, call the content handler and return.
  5. Create a UNNotificationAttachment with the file URL. Name the identifier image to set it as the image on the final notification.
  6. If creating the attachment succeeds, add it to the attachments property on bestAttemptContent.
  7. Call the content handler to deliver the push notification.

Build and run. Send the app to the background.

Now, send another push from the Push Notifications Tester app with the same payload. You should see the push come in with an image in the top right corner:

Modified push with image

Pull down the notification. You’ll see it expands and uses the image to fill a large part of the screen:

Push with expanded image

Awesome! You are now able to update your notification content. Next, you’ll go further by creating custom UI around your push content.

Creating a Custom UI

You can take rich notifications a step further by adding a custom UI on the top of your push content. This interface will replace the standard push notification UI by way of an app extension.

Content Extension interaction

This interface is a view controller that conforms to UNNotificationContentExtension. By implementing didReceive(_:), you can intercept the notification and set up your custom interface.

Adding the Target

Like before, click on FileNewTarget…, and filter for the Notification Content Extension:

Adding a notification content extension

Name the content extension WendercastNotificationContent and ensure the fields are proper (using your own team and organization name):

Configuring a notification content extension

This time, click Activate on the schema activation confirmation screen.

Configuring Info.plist

Next, you need to configure the Info.plist of the new target to display your content extension.

  1. In the WendercastNotificationContent group, open Info.plist.
  2. Expand NSExtension dictionary.
  3. Expand the NSExtensionAttribute dictionary.
  4. Update the value in UNNotificationExtensionCategory to new_podcast_available.
  5. Click the + to add a new key-value pair to the NSExtensionAttribute dictionary.
  6. Add the key UNNotificationExtensionDefaultContentHidden as a Boolean, and set the value to YES.
  7. Add one more Boolean key-value pair. Set the key to UNNotificationExtensionUserInteractionEnabled and the value to YES.

Your final Info.plist should look like this (the order of the keys doesn’t matter):

Final info plist setup

Here’s what these parameters are for:


  • The value of this entry must match a value in the incoming push content. iOS needs this to determine which UI to use for displaying the notification.
  • You need it because you may want to provide custom UIs for different categories of push notifications.
  • If this value is missing, iOS will not invoke your extension.


  • This value is a number between 0 and 1. It represents the aspect ratio of your custom interface.
  • The default of 1 tells iOS that your initial interface height is the same as its width.
  • For example, if you set this value to 0.5, then this would tell iOS that the height of your interface is half the size as its width.
  • This is an estimate and allows iOS to set the initial size of your interface, preventing unnecessary resizing.


  • When set to YES, the standard title, subtitle and body of the push content are not visible.
  • When set to NO, the standard push content displays beneath the custom UI.


  • When set to YES, it enables user interaction with UIKit elements.

Adding the App Group

Add the same app group you created for the main app target:

  1. In the File navigator, click the project node.
  2. Select the WendercastNotificationContent target.
  3. Select the Signing & Capabilities tab.
  4. Click + Capability.
  5. Select App Groups.
  6. Select the same app group ID you created at the beginning of this tutorial.

Building the Custom UI

You probably want to focus on building the content extension logic, not waste time on tedious Interface Builder shenanigans. To give you the assist, the download content for this tutorial already contains a ready-to-be-used storyboard. Feel free to use it to replace the storyboard automatically created by Xcode.

Here’s how to do that:

  1. In Xcode, delete MainInterface.storyboard from the WendercastNotificationContent group. Choose Move to Trash when prompted.
  2. In the download materials, drag the MainInterface.storyboard file from the ContentStoryboard folder into Xcode in the WendercastNotificationContent folder.
  3. Check the Copy items if needed box and select the WendercastNotificationContent target in the Add to targets list.
  4. Click Finish.
  5. Open the storyboard in Xcode.

You’ll see this storyboard provides a good starting point for the notification UI. It provides UI elements for a title, podcast image, favorites button and play button. As a bonus, the auto layout constraints are already set up.

You can now focus on building the view controller.

Setting up NotificationViewController

NotificationViewController is responsible for presenting the custom notification view for your users. You’ll make the modifications necessary to present your awesome new push notification view :].

Adding Shared Files

Open the following files and add WendercastNotificationContent to their target membership in the File inspector:

  • CoreDataManager.swift
  • PodcastItem.swift
  • Podcast.swift
  • DiskCacheManager.swift
  • Wendercast.xcdatamodel

You do this by checking the WendercastNotificationContent box.

Setting up target membership

This will make the data model and networking classes available to the content extension.

Note: Xcode may be confused at this point, showing a number of errors. You can clear them by simply pressing Command-B to build the target.

Customizing the UI

Look under the class declaration in NotificationViewController. Remove this code automatically generated by Xcode:

  • label
  • viewDidLoad()
  • didReceive(_:)‘s body

Then add the following outlets right under the class declaration:

@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var podcastTitleLabel: UILabel!
@IBOutlet weak var podcastBodyLabel: UILabel!

And then add a property to hold the current podcast:

var podcast: Podcast?

Lastly, add the following convenience method to load a podcast from the shared data store:

private func loadPodcast(from notification: UNNotification) {
  // 1
  let link = notification.request.content.userInfo["podcast-link"] as? String

  // 2
  guard let podcastLink = link else {

  // 3
  let podcast = CoreDataManager.shared.fetchPodcast(
    byLinkIdentifier: podcastLink)

  // 4
  self.podcast = podcast

Here’s what you’re doing above:

  1. Try to get the link to the podcast from the userInfo object attached to the notification. The podcast link is the podcast’s unique identifier in the Core Data store.
  2. If the link does not exist, return early.
  3. Use the link to fetch a Podcast model object from the Core Data store.
  4. Set podcast with a response.

Customizing the Notification

Replace the body of didReceive(_:) with the following:

// 1
loadPodcast(from: notification)

// 2
let content = notification.request.content
podcastTitleLabel.text = content.subtitle
podcastBodyLabel.text = content.body

// 3
  let attachment = content.attachments.first,
  else {

// 4
let fileURLString = attachment.url

  let imageData = try? Data(contentsOf: fileURLString),
  let image = UIImage(data: imageData) 
  else {

// 5
imageView.image = image

Once a push notification comes in, here’s what you are doing above:

  1. Call the convenience method to load the podcast from the Core Data store. This sets podcast for use later.
  2. Set the title and body labels to the values received from the push notification.
  3. Attempt to access the media attached to the service extension. If not, return early. The call to startAccessingSecurityScopedResource() allows you to access the attachment.
  4. Get the URL for the attachment. Attempt to retrieve it from disk and convert the data to an image. If it fails, return early.
  5. If the image retrieval is successful, set the podcast image and stop accessing the resource.

Implementing the Favorite Action

The UI has a button to add the notified podcast to the list of favorites. This is a perfect example of how to make a push notification actionable and cool.

Add the following method to handle the tap on the favorite button:

@IBAction func favoriteButtonTapped(_ sender: Any) {
  // 1
  guard let podcast = podcast else {

  // 2
  let favoriteSetting = podcast.isFavorite ? false : true
  podcast.isFavorite = favoriteSetting

  // 3
  let symbolName = favoriteSetting ? "star.fill" : "star"
  let image = UIImage(systemName: symbolName)
  favoriteButton.setBackgroundImage(image, for: .normal)

  // 4

To handle the favorite button tap, you are:

  1. Checking to make sure a podcast has been set.
  2. Toggling isFavorite on podcast.
  3. Updating the favorite button UI to match the model state.
  4. Updating the Core Data store with the changes.

This is enough to test your first set of changes to the content extension. Set the scheme back to Wendercast, then build and run, and put the app to the background. Next, send the following content from the Push Notifications Tester app:

  "aps": {
    "category": "new_podcast_available",
    "alert": {
      "title": "New Podcast Available",
      "subtitle": "Antonio Leiva – Clean Architecture",
      "body": "This episode we talk about Clean Architecture with Antonio Leiva."
    "mutable-content": 1
  "podcast-image": "https://koenig-media.raywenderlich.com/uploads/2016/11/Logo-250x250.png",
  "podcast-link": "https://www.raywenderlich.com/234898/antonio-leiva-s09-e13",
  "podcast-guest": "Antonio Leiva"

Once the notification comes in, pull down on it. You’ll see your updated custom interface:

Content extension UI

Tap the Favorite button, and you’ll see it change its state. If you open the Wendercast app to the same podcast, you’ll notice that the state of the favorite button matches that of the notification UI. Awesome!

Final content UI

Implementing the Play Action

Now you’ll implement a deep link into the app for the play action. Add the following method to NotificationViewController:

@IBAction func playButtonTapped(_ sender: Any) {

This tells the notification extension to open the application and deliver everything as a standard push.

Next, look at the extension in Wendercast/App/SceneDelegate.swift. This code performs a lot of the same work you’ve been doing in the extensions:

  • Looks for the presence of a podcast link.
  • Attempts to fetch the podcast from the Core Data store.
  • Tells the PodcastFeedTableViewController to load the specified podcast.
  • Plays the specified podcast

Build and run. Send the app to the background, and push the same notification payload you sent last time. This time, tap the Play button. The app will deep link into the podcast detail and begin streaming the episode. You’ve done it!

Where to Go From Here?

Congratulations! You’ve taken a deep dive into rich push notifications. You learned how to:

  • Modify push content
  • Attach media
  • Create custom UIs
  • Navigate interaction between your extensions and their host app

To learn even more, you can study notification actions and how they apply to notification content extensions. With customizable UI and user interaction enabled, the possibilities are practically endless! To get started, check out the official documentation:

You can download the completed project files by clicking on the Download Materials button at the top or bottom of the tutorial.

If you have any comments or questions, please join the forums below!