Custom Interactive Remote Push Notification trong iOS

Introduction

Kể từ iOS 10, Apple đã cung cấp rich notification hỗ trợ push notification với các framework mới: UserNotificationsUserNotificationsUI.

Khi sử dụng các framework này, chúng ta có thể customize thông báo push notification với các khả năng:

  • Customize type và content/UI của push notification.
  • Cung cấp các custom action và response tương ứng cho mỗi loại notification.
  • Thay đổi content của các push notification trước khi hiển thị cho user.
  • Customize các custom trigger của push notification theo các khoảng thời gian cụ thể hay theo các khu vực địa lý.

Bên cạnh việc support rich notification, Apple cũng đã hỗ trợ thêm interactive custom UI push notification trong iOS 13. Trước đây, chúng ta chỉ có thể customize các action mà user chỉ có thể tương tác giống như dạng action-sheet trên iPhone UI. Với kiểu interactive notification mới, chúng ta còn có thể hiển thị rất nhiều kiểu UI và tương tác trên giao diện push notification.

Đây thực sự là một thay đổi lớn cho phần push notification. Ví dụ, chúng ta có thể cung cấp các text fieldm switch, slider, stepper hoặc bất kỳ custom control nào mà ta muốn, có thể tùy ý customize push notification preview một cách dễ dàng.

Trong bài viết này, chúng ta sẽ xây dựng một interactive custom push notification UI để hiển thị một video preview Youtube với các button mà user có thể tương tác. Ngoài ra, user cũng có thể rate video và comment trên một text field. Cụ thể các task cần làm như sau:

  • Setup project và các dependencies.
  • Require push notification permission, type và category.
  • Giả lập test push notification.
  • Setup Notification Extension target infoplist.
  • Setup UI cho notification content.
  • Handle notification nhận được và các tương tác của user.

Để có thể giả lập test push notification trên simulator, chúng ta cần download và cài đặt tối thiểu Xcode 11.4 trở lên từ Apple developer website

Setup project and dependencies

Để bắt đầu, hãy tạo mới một Xcode project với một unique bundle identifier. Sau đó, chuyển đến tab Signing & Capabilities của project. Click button + Capabilities rồi chọn push notification. Mục đích là enable tính năng push notification vào App ID của project.

Tiếp theo, chúng ta tạo một app extension target mới cho custom content notification UI. Từ menu bar, click File > New > Target. Gõ notification trong text field filter rồi chọn Notification Content Extension. Đặt tên cho nó và click finish.

Sau đó, activate scheme vừa tạo trong alert dialog, rồi đóng project.

Tiếp tục, chúng ta sẽ init Cocoapods cho project và khai báo các dependencies cần sử dụng. Mở terminal, navigate đến folder project hiện tại và gõ lệnh pod init. Sau đó, mở Pod file vừa tạo bằng text editor và thêm các dependencies sau:

  1. XCDYoutubeKit: Một library cho phép play video Youtube sử dụng AVPlayer
  2. Cosmos: Một library custom control hiển thị rating dạng star.
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'iRPC' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iRPC
  pod "XCDYouTubeKit", "~> 2.9"
  pod 'Cosmos', '~> 21.0'

end

target 'iRPCNotificationExtension' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iRPCNotificationExtension
  pod "XCDYouTubeKit", "~> 2.9"
  pod 'Cosmos', '~> 21.0'

end

Trong terminal, chạy lệnh pod install để install các dependencies trên vào các target. Sau khi hoàn tất, mở file .xcworkspace với Xcode và build thử Command + B để đảm bảo mọi thứ hoạt động bình thường.

Register push notification

Tiếp theo, chúng ta cần phải register permission để cho phép push notification trong app sử dụng class UNUserNotificationCenter. Import framework UserNotifications trong AppDelegate và thêm code trong method (_:didFinishLaunchingWithOptions:).

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 notificationCategory = UNNotificationCategory(identifier: "iRPCNotificationCategory", actions: [], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([notificationCategory])
        return true
    }
   
}
  1. Đầu tiên, gọi method requestAuthorization và truyền vào mảng các option. Trong trường hợp này chúng ta cần alert, badgesound.
  2. Thứ hai, init UNNotificationCategory và truyền vào một unique category identifier string. Ở đây, do chúng ta không cần các custom action nên truyền vào mảng action rỗng.
  3. Cuối cùng, gọi method setNotificationCategories và truyền vào custom notification category vừa tạo vào.

Simulate remote push notification for testing

Kể từ Xcode 11.4 thì Apple cuối cùng cũng đã cung cấp tính năng mới cho phép giả lập push notification ở local. Cách làm rất đơn giản. Chúng ta chỉ cần tạo một file apns json chứa content payload. Ngoài ra chúng ta cũng cần phải thêm một số key khác bên cạnh key aps. Đó là key Simulator Target Bundle với value là App ID đang cần push notification. File dạng json nhưng filename extension là .apns, ví dụ như sau:

{
   "aps" : {
      "alert" : {
         "title" : "米津玄師 MV「LOSER」",
         "body" : "5th Single「LOSER / ナンバーナイン」2016.9.28 RELEASE"
      },
      "category" : "iRPCNotificationCategory",
      "sound": "bingbong.aiff",
      "badge": 3,
   },
   "videoId" : "Dx_fKPBPYUI",
   "description": "5th Single「LOSER / ナンバーナイン」2016.9.28 RELEASE",
   "Simulator Target Bundle": "com.thanhfnx.iRPC"
}

Như trên, chúng ta có 2 key videoIddescription nhằm mục đích để lấy được URL video Youtube và thông tin hiển thị thêm trên custom UI. Để test push notification trên simulator, run app, accept notification permission. Sau đó, ẩn app xuống backgroun và kéo thả file .apns vừa tạo vào simulator đang chạy.

Setup Notification Extension target infoplist

Chúng ta cần thêm các key cho notification content extension trong Info.plist. Trong dictionary NSExtension > NSExtensionAttributes, hãy thêm các key và value sau:

  1. UNNotificationExtensionDefaultContentHidden: Key này xác định khi nào thì ẩn title và body label của push notification mặc định. Trong ví dụ này, chúng ta muốn ẩn chúng, vì vậy hãy set là YES.
  2. UNNotificationExtensionUserInteractionEnabled: Key này xác định có enable interactive UI trên push notification hay không. Hãy set nó là YES.
  3. UNNotificationExtensionCategory: Key này chỉ định notification type category identifier đã register ở AppDelegate.
  4. UNNotificationExtensionInitialContentSizeRatio: Content size ratio mặc định khi preview UI được hiển thị lần đầu, giá trị mặc định là 1.

Setup UI for the notification content preview

Công việc tiếp theo là tạo preview UI cho notification. Mở file MainInterface.storyboard trong target notification extension và customize UI tùy theo ý muốn.

Handle receive notification and UI interaction

Mở file NotificationViewController.swift, trong file này sẽ có class NotificationViewController là sub class của UIViewController và conform UNNotificationContentExtension. Method didReceive(_:) sẽ được gọi khi nhận được push notification qua payload. Code handle các view, setup property khi nhận notification và interactive như sau:

  1. Import tất cả các framework cần dùng như: AVKit, UserNotifications, UserNotificationsUI, XCYoutubeKit, và Cosmos ở đầu file.
  2. Khai báo tất cả các property IBOutlet cho các label, button, view, và text view.
  3. Khai báo các method IBAction cho các button.
  4. Khai báo các property lưu trạng thái của isSubscibedisFavorited với property observer. Trong trường hợp này, text và màu của các button sẽ thay đổi dựa trên giá trị các biến bool trên.
  5. Khai báo các constant lưu giá trị height của các view.
  6. Trong method viewDidLoad, setup các initial view state cho các button và các stack view. Review stack view và submit label sẽ bị ẩn.
  7. Trong method didReceive(_:), chúng ta sẽ nhận được content và set text cho label video title và label video description từ payload. Ngoài ra lấy được videoId và sử dụng XCDYouTubeClient để get URL của video Youtube.
  8. Sau khi lấy được URL, khởi tạo AVPlayerViewController bằng URL trêb rồi hiển thị và play video trong Player View.
  9. Khi tap vào button review, hiển thị phần stack view review. Khi tap vào button submit, ẩn stack view review và hiển thị thông báo bằng label submit. Đối với các button subscribe và favorite, khi tap vào sẽ đổi text và màu của các button này.
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.title
        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()
    }

}

Build và run project, kéo thả file .apns đã chuẩn bị ở trên vào simulator để test push notification, ta được kết quả như sau:

Conclusion

Trên đây chỉ là một ví dụ đơn giản cho việc custom các interactive push notification UI. Có rất nhiều khả năng và use case mà bạn có thể sáng tạo và ứng dụng. Khác biệt với những dòng thông báo notification đơn điệu, nhàm chán, custom interactive push notification mang lại những trải nghiệm người dùng mới, giúp tăng tương tác giữa user và app của bạn.


All Rights Reserved