Building Custom Interactive Remote Push Notification in iOS
Published at Feb 17, 2020
Since iOS 10, Apple has already provided rich notification
support for push notification with the introduction of new frameworks, UserNotifications
and UserNotificationsUI.
Using this framework, we can customize our push notification with abilities such as:
- Customize type and
content/UI
of push notification. - Provide custom actions and responses for each of the types of notification.
- Mutate the content of received push notification before it delivered to the user.
- Customize custom trigger of the push notification such as in specific time interval and geographic region.
Besides all this rich notification support, Apple also added new interactive custom UI support in iOS 13. Before, we can only customize the actions for the user to select in action-sheet like iPhone UI. With the interactive notification, we can provide any user interface and interaction that we want in our push notification.
This addition is a real game-changer for push notification. For example, we can provide controls like text field, switch, slider, stepper, and any custom control that we want. Finally, we have the freedom to customize our push notification preview the way we want it to be.
In this article, we are going to build an interactive custom push notification UI to display a video preview of a trailer with buttons for users to add and favorite. Users can also provide ratings using the star and comment using a text field. Here is the list of tasks to do:
- Setup project and dependencies.
- Register push notification permission, type, and category.
- Simulate remote push notification for testing.
- Setup Notification Extension target info plist.
- Setup UI for the notification content.
- Handle on receive notification and UI interaction in code.
To simulate a remote push notification in the simulator, you need to download and install
Xcode 11.4
beta from Apple developer website
You can download the completed project from the GitHub repository here.
Setup Project and Dependencies
To begin, create a new Xcode project using your unique Bundle Identifier. Then, navigate to the project signing & capabilities tab. Click on + Capabilities
button and select push notifications from the list. The purpose is to associate the push notification with the App ID of the project.
Next, we are going to add a new app extension target for the custom content notification UI. From the menu bar, click on File > New > Target
. On the filter text field, type notification. Finally, select Notification Content Extension from the list. Give the name and click finish. After that, you can activate the scheme when in the alert dialog. Close the project.
Next, we are going to initialize Cocoapods
for the project and declare the dependencies. Using the terminal, navigate to the project folder, and type pod init
. After that, open the Podfile
using your text editor and add the dependencies to the targets of the app. There are 2 dependencies to add:
- XCDYoutubeKit. A library to play YouTube video using the
AVPlayer
. (warning: YouTube policy only allow the app to useWebView
withiFrame
to play video in-app). - Cosmos. A star rating control that we can use in our app.
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'apnspushsimulate' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for apnspushsimulate
pod "XCDYouTubeKit", "~> 2.9"
pod 'Cosmos', '~> 21.0'
end
target 'test' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for test
pod "XCDYouTubeKit", "~> 2.9"
pod 'Cosmos', '~> 21.0'
end
Run pod install
to install all the dependencies to all the targets. Open the project.xcworkspace
in your Xcode to begin. Try to build with Command + B
to make sure everything is good to go.
Register permission, type, and category for push notification
Next, we need to register permission to allow push notification in our app using the UNUserNotificationCenter
. We add the code to do that in the AppDelegate
inside the (_:didFinishLaunchingWithOptions:)
. Make sure to import the UserNotifications
framework at the top of the source file. Here are the steps to do:
- First, we invoke the
requestAuthorization
passing the array of authorization options. In this case, we want thealert
,badge
, andsound
. - Second, we initialize the
UNNotificationCategory
passing the unique category string identifier. In this case, we don't want to have custom actions, so we pass an empty array. - Last, we invoke the
setNotificationCategories
passing our custom notification category type inside an array.
import UIKit
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
let testNotificationCategory = UNNotificationCategory(identifier: "testNotificationCategory", actions: [], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([testNotificationCategory])
return true
}
...
}
Simulate remote push notification for testing
Xcode 11.4 finally introduced a new feature to simulate remote push notification locally. It's pretty simple to begin. We need to create the apns json
file containing the payload. We also need to add additional key beside the aps
, which is the Simulator Target Bundle
containing the App ID of our app. The filename extension of the file must be using .apns
instead of .json
. You can take a peek of our sample apns
file below.
{
"aps" : {
"alert" : {
"title" : "A new trailer has arrived for you",
"body" : "Fast and Furious F9 Official Trailer"
},
"category" : "testNotificationCategory",
"sound": "bingbong.aiff",
"badge": 3,
},
"videoId" : "Kopyc23VfSw",
"description": "Release date: 05-21-2020",
"Simulator Target Bundle": "com.alfianlosari.apnspushsimulate"
}
In this case, we also provide the additional key for videoId
and description
so we can retrieve the YouTube
video URL and display custom description in the custom UI. To test the notification, run the app, and accept the notification permission. Then, put the app in background, drag, and drop the apns
file into the simulator.
Setup Notification Extension target info plist.
We need to add additional keys to the notification content
application extension info.plist
. Under the NSExtension > NSExtensionAttributes
dictionary make sure you add all these keys and values:
UNNotificationExtensionDefaultContentHidden
. This key determines whether to hide the default push notification title and body labels. In our case, we want to hide it, so we set the value toNO
.UNNotificationExtensionUserInteractionEnabled
. This key determines whether to make the UI interactive. We set this toYES
.UNNotificationExtensionCategory
. We need to set the value of this using the notification type category identifier that we register at theAppDelegate
, which istestNotificationCategory
.UNNotificationExtensionInitialContentSizeRatio
. The initial content size ratio when the preview appears the first time. We set this to default,1
.
Setup UI for the notification content preview
Let's move to the notification content preview UI. Open the MainInterface.storyboard
in the notification extension target. Here are the steps to do:
- Drag a view to the View Controller canvas. Add these constraints:
Align top, leading, and trailing to Safe Area
. Set theheight constraints
to240
. Rename this view toPlayer View
. This is the video player view that embeds theAVPlayer
View. - Drag a
Stack View
to the View Controller below thePlayer View
. Add these constraints:Top Space to Player View with 16
.Align leading, trailing, bottom to Safe Area
. Set theaxis
tovertical
,alignment and distribution
tofill
, and setspacing
to16
. Rename this toOuter Stack View
. - Drag a
Stack View
as a subview of theOuter Stack View
. Add 2 labels inside this stack view,video title label
andvideo description label
. Set theaxis
tovertical
,alignment and distribution
tofill
, and setspacing
to4
. Set thevideo title label
line limit to2
. - Drag a
Stack View
as a subview of theOuter Stack View
. Add 2 buttons inside this stack view,subscribe button
andfavorite button
. Set theaxis
tohorizontal
,alignment
tofill
,distribution
tofill equally
, and setspacing
to4
. - Drag a
Button
as a subview of theOuter Stack View
, rename this toReview Button
. Set the text toReview
. - Drag a
Label
as a subview of theOuter Stack View
, rename this toSubmit Label
. Set the text toYour review has been submitted!
- Drag a
Stack View
as a subview of theOuter Stack View
. Set theaxis
tovertical
,alignment and distribution
tofill
, and setspacing
to24
. Rename this toReview Stack View
. - Drag a
View
as a subview of theReview Stack View
. Set the class toCosmosView
. In the attribute inspector, set thestart margin to 16
andstar size to 50
. Set theheight constraint to 59
. - Drag a
Stack View
as a subview of theReview Stack View
, below theCosmos View
. Add a label inside this stack view,comment label
,TextView
, andButton
. Set theaxis
tovertical
,alignment and distribution
tofill
, and setspacing
to8
. Set theheight constraint
to100
. Make sure to set theComment label
text tocomment
. - Drag a
Button
as a subview of theReview Stack View
at the bottom. Set thetext
toSubmit
, andconstraint the height
to40
.
Handle on receive notification and UI interaction in code.
Open the NotificationViewController.swift
file. There is a NotificationViewController
class that subclass UIViewController
and implements the UNNotificationContentExtension
. The didReceive(_:)
method is invoked when the push notification arrives passing the payload. Here is the brief overview of the code handling for the view, properties setup, didReceive
and interaction handler:
- Imports all the required frameworks,
AVKit
,UserNotifications
,UserNotificationsUI
,XCYoutubeKit
, andCosmos
at the top of the source file. - You need to also declare all the properties for labels, buttons, views, and text view.
- Declares all the
@IBAction
method handling when the user taps on the respective button. In this case, subscribe, favorite, review, and submit handler. - Declares the properties for storing the state of
isSubscribed
andisFavorited
with the property observer. In this case, the text and color of the button change depending on theboolean
state. - Declare the constants for storing the height of the view. In sake of this example, i already calculated the possible height when the view is expanded or collapsed.
- In
viewDidLoad
, set up the initial view state of buttons and stack views. TheReview Stack View
andSubmit Label
are hidden; the first time view appears. - In the
didReceive
, we retrieve the content and set the text of labels for title and description. Also, we get thevideoID
and use theXCDYouTubeClient
to get the video URL passing the identifier. - After we successfully retrieve the
URL
, initializeAVPlayerViewController
with the URL, embed the view inside thePlayer View
container view, and play the content. - When the user tap on review button, unhide the
Review Stack View
. When they tap on the submit button, hide theReview Stack View
and show thesubmit label
. For subscribing and favoriting, toggle the property to update the text and color of the buttons.
import UIKit
import AVKit
import UserNotifications
import UserNotificationsUI
import XCDYouTubeKit
import Cosmos
class NotificationViewController: UIViewController, UNNotificationContentExtension {
@IBOutlet weak var playerView: UIView!
@IBOutlet weak var reviewStackView: UIStackView!
@IBOutlet weak var reviewButton: UIButton!
@IBOutlet weak var videoTitleLabel: UILabel!
@IBOutlet weak var videoDescriptionLabel: UILabel!
@IBOutlet weak var submitLabel: UILabel!
@IBOutlet weak var subscribeButton: UIButton!
@IBOutlet weak var favoriteButton: UIButton!
var playerController: AVPlayerViewController!
let standardHeight: CGFloat = 432
let reviewHeight: CGFloat = 658
var isSubscribed = false {
didSet {
self.subscribeButton.tintColor = self.isSubscribed ? UIColor.systemGray : UIColor.systemBlue
self.subscribeButton.setTitle(self.isSubscribed ? " Added" : " Add", for: .normal)
}
}
var isFavorited = false {
didSet {
self.favoriteButton.tintColor = self.isFavorited ? UIColor.systemGray : UIColor.systemBlue
self.favoriteButton.setTitle(self.isFavorited ? " Favorited" : " Favorite", for: .normal)
}
}
override func viewDidLoad() {
super.viewDidLoad()
subscribeButton.setImage(UIImage(systemName: "calendar"), for: .normal)
favoriteButton.setImage(UIImage(systemName: "star"), for: .normal)
reviewButton.setImage(UIImage(systemName: "pencil"), for: .normal)
reviewStackView.isHidden = true
submitLabel.isHidden = true
}
func didReceive(_ notification: UNNotification) {
playerController = AVPlayerViewController()
preferredContentSize.height = standardHeight
videoTitleLabel.text = notification.request.content.body
videoDescriptionLabel.text = notification.request.content.userInfo["description"] as? String ?? ""
guard let videoId = notification.request.content.userInfo["videoId"] as? String else {
self.preferredContentSize.height = 100
return
}
XCDYouTubeClient.default().getVideoWithIdentifier(videoId) { [weak self] (video, error) in
guard let self = self else { return }
if let error = error {
print(error.localizedDescription)
return
}
guard let video = video else {
return
}
let streamURLS = video.streamURLs
if let url = streamURLS[XCDYouTubeVideoQuality.medium360] ?? streamURLS[XCDYouTubeVideoQuality.small240] ?? streamURLS[XCDYouTubeVideoQuality.HD720] ?? streamURLS[18] {
self.setupPlayer(with: url)
}
}
}
private func setupPlayer(with url: URL) {
guard let playerController = self.playerController else {
return
}
let player = AVPlayer(url: url)
playerController.player = player
playerController.view.frame = self.playerView.bounds
playerView.addSubview(playerController.view)
addChild(playerController)
playerController.didMove(toParent: self)
player.play()
}
@IBAction func submitTapped(_ sender: Any) {
UIView.animate(withDuration: 0.3) {
self.reviewStackView.isHidden = true
self.submitLabel.isHidden = false
self.preferredContentSize.height = self.standardHeight
}
}
@IBAction func reviewTapped() {
UIView.animate(withDuration: 0.3) {
self.preferredContentSize.height = self.reviewHeight
self.reviewStackView.isHidden = false
self.reviewButton.isHidden = true
}
}
@IBAction func subscribeTapped(_ sender: Any) {
self.isSubscribed.toggle()
}
@IBAction func favoriteTapped(_ sender: Any) {
self.isFavorited.toggle()
}
}
Before building and run, make sure to connect all the @IBOutlets
and @IBActions
from the storyboard to the code. To test, just drag and drop the apns
file to the simulator, make sure to use the correct App ID. You can try to play with the videoId
value of the file by using your video identifier. You can retrieve this from the URL parameters of the YouTube video in the browser address bar.
Conclusion
That's it! Congrats on your achievement on building the custom interactive push notification UI. There are many possibilities and use cases that you can explore using this new capability. To handle sharing data between the main app and extension target, you need to create app group id
. Then, you can initialize a shared UserDefaults
to store and retrieve local data. You can explore more about the UserNotifications
framework from Apple documentation
Let's keep the lifelong learning goes on!