系统推送的集成(十七) —— APNs从工程配置到自定义通知UI全流程解析(一)

版本记录

版本号 时间
V1.0 2020.05.16 星期六

前言

我们做APP很多时候都需要推送功能,以直播为例,如果你关注的主播开播了,那么就需要向关注这个主播的人发送开播通知,提醒用户去看播,这个只是一个小的方面,具体应用根据公司的业务逻辑而定。前面已经花了很多篇幅介绍了极光推送,其实极光推送无非就是将我们客户端和服务端做的很多东西封装了一下,节省了我们很多处理逻辑和流程,这一篇开始,我们就利用系统的原生推送类结合工程实践说一下系统推送的集成,希望我的讲解能让大家很清楚的理解它。感兴趣的可以看上面几篇。
1. 系统推送的集成(一) —— 基本集成流程(一)
2. 系统推送的集成(二) —— 推送遇到的几个坑之BadDeviceToken问题(一)
3. 系统推送的集成(三) —— 本地和远程通知编程指南之你的App的通知 - 本地和远程通知概览(一)
4. 系统推送的集成(四) —— 本地和远程通知编程指南之你的App的通知 - 管理您的应用程序的通知支持(二)
5. 系统推送的集成(五) —— 本地和远程通知编程指南之你的App的通知 - 调度和处理本地通知(三)
6. 系统推送的集成(六) —— 本地和远程通知编程指南之你的App的通知 - 配置远程通知支持(四)
7. 系统推送的集成(七) —— 本地和远程通知编程指南之你的App的通知 - 修改和显示通知(五)
8. 系统推送的集成(八) —— 本地和远程通知编程指南之苹果推送通知服务APNs - APNs概览(一)
9. 系统推送的集成(九) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 创建远程通知Payload(二)
10. 系统推送的集成(十) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 与APNs通信(三)
11. 系统推送的集成(十一) —— 本地和远程通知编程指南之苹果推送通知服务APNs - Payload Key参考(四)
12. 系统推送的集成(十二) —— 本地和远程通知编程指南之Legacy信息 - 二进制Provider API(一)
13. 系统推送的集成(十三) —— 本地和远程通知编程指南之Legacy信息 - Legacy通知格式(二)
14. 系统推送的集成(十四) —— 发送和处理推送通知流程详解(一)
15. 系统推送的集成(十五) —— 发送和处理推送通知流程详解(二)
16. 系统推送的集成(十六) —— 自定义远程通知(一)

开始

首先看下主要内容

学习如何在推送通知呈现给用户之前修改和增强推送通知,如何围绕推送内容创建自定义UI,等等!内容来自翻译 。

接着看下写作环境

Swift 5, iOS 13, Xcode 11

下面就是正文了。

如果你在过去十年里一直使用移动设备,你可能会遇到无数的推送通知。推送通知允许应用程序向用户广播alerts —— 即使他们不积极使用设备。

虽然通知可以向用户提供有用的信息,但是通知的真正威力来自于称为富通知(rich notifications)的概念。富通知允许您拦截通知负载(payloads),并为您提供时间以最适合用户需求的方式对其进行修饰(自己加的:就像历史一样,任人装扮)。这允许您显示自定义UI,其中可以包含为用户提供快捷方式的按钮操作。

本教程假设您对推送通知有一定的了解。

本教程将进一步介绍这些知识。您将学习如何修改和增强传入的内容,如何围绕您的推送内容创建自定义UI和更多!

因为推送通知只能在物理设备上工作,所以您需要使用几个属性来配置Apple developer portal,以便在本教程中使用。Xcode可以通过自动配置为您处理大部分问题。

1. Setting up New App Values

在Xcode中打开starter起始项目。如果您还没有登录到您的development team,请转到Preferences,选择Accounts选项卡,然后使用+按钮登录。

接下来,在文件导航器File navigator中选择Wendercast项目节点。确保选择了Wendercast目标。在中间的窗格中,转到Signing & Capabilities选项卡并选中Automatically manage signing框。

设置以下值:

  • 1) 从team下拉列表中选择您的开发团队。
  • 2) 在Bundle Identifier中,输入唯一的Bundle ID
  • 3) 在App Groups下,点击+。保留group前缀,并输入上一步中使用的相同bundle ID
  • 4) 确保Push Notifications出现在capabilities列表中。

如果您看到两个不同的Signing (debug)Signing (release)部分,请在Automatically manage signing, TeamBundle Identifier这两个部分上进行配置。使用相同的值。

最后,打开DiskCacheManager.swift。然后将groupIdentifier属性更新为您的新app group ID

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

2. Creating an Authentication Key

对于这一步,登录到Apple developer门户Apple developer portal。单击Account选项卡,然后按照以下说明操作:

  • 1) 从左侧侧边栏选择Certificates, ID, & Profiles
  • 2) 从Certificates, ID, & Profiles屏幕中选择Keys
  • 3) 单击+按钮。
  • 4) 填写Key Name字段,选择Apple Push Notifications Service (APNs),然后点击Continue
  • 5) 点击Register
  • 6) 记下您的Key ID。您将在以后的步骤中需要它。
  • 7) 单击Download.p8文件保存到磁盘。
  • 8) 再次记下你的team ID,它显示在页面的右下角(在您的姓名或公司名称下面)。

这已经是很多了。现在您已经完成了门户的工作,是时候重新配置starter应用程序了。

这一步将允许应用程序读写共享应用程序容器。在添加扩展(extension)时,这是必要的,这样应用程序和扩展(app and extension)都可以访问Core Data store

你将创建不是一个而是两个扩展,应用程序将使用Core Data


Running the App

Wendercast应用程序获取并解析Ray Wenderlich的播客feed。然后在UITableView中显示结果。用户可以点击任何一集,打开一个详细视图,开始播放该集。他们也有能力喜欢任何一集。

如前所述,应用程序使用Core Data在会话之间进行持久化。在feed刷新期间,应用程序只插入新剧集。

首先,确保您的iPhone是所选设备。然后构建并运行。您应该会看到一个podcast列表和一个启用通知的即时提示。利用Allow

点击列表中的任何一集,你就会看到一个详细的屏幕。选择的播客应该立即开始播放。

现在,您将发送一个测试推送通知(push notification)

打开Xcode控制台。你应该看到device token打印在日志:

Permission granted: true
Notification settings:
  
Device Token: [[device-token-here]]

保存这个值,因为您现在就需要它。

1. Testing a Push Notification

在测试推送通知之前,您需要能够发送它们。

发送推送通知需要调用Apple Push Notification Server (APNS)上的REST API。这绝对不是最简单的方法——尤其是当你必须手工操作的时候。

幸运的是,还有另一种方法。让一个应用程序为你做。

遵循这些步骤:

  • 1) 下载并安装Push Notifications Tester app。
  • 2) 启动app

注意:如果macOS提示无法启动应用程序,在Finder中右键单击应用程序并选择Open

  • 3) 选择iOS选项卡。
  • 4) 选择Authentication下的Token
  • 5) 单击Select P8,然后选择保存到磁盘的. p8文件。
  • 6) 在Enter key id区域中输入.p8 key ID。您在下载p8文件之前复制了它。
  • 7) 在Enter team id区域中输入您的team ID
  • 8) 在Body下输入应用程序的bundle ID
  • 9) 输入上一步保存的device token
  • 10) 保留Collapse id为空。
  • 11) 将有payload按照原样放在body中。

结果,应该是这样的:

将应用程序放置到设备后台,但不锁设备。在Push Notifications Tester应用程序中点击Send。你会收到一个推送通知和一条成功消息。

现在,您已经完成了所有的设置,现在是深入研究代码的时候了。


Modifying Push Content

苹果创造了一种方法,可以在service extensions交付之前修改推送内容。Service extensions允许您拦截来自APNS的推送内容,对其进行修改,然后将修改后的有效负载(payload)交付给用户。

Service extensions位于APNS server和推送通知的最终内容之间:

1. Introducing Service Extensions

service extension获得有限的执行时间来对传入的推送负载执行一些逻辑。你可以做的一些事情来修改和增加推送负载:

  • 更新推送的标题、副标题或主体(body)
  • 添加媒体附件(attachment)到推送。

2. Adding a Service Extension

回到Wendercast项目,通过点击File ▸ New ▸ Target…创建一个新的target

选择Notification Service Extension并单击Next

将扩展名为WendercastNotificationService。字段应该是这样的:

验证字段输入之后,单击Finish。如果出现提示,不要激活新scheme

这样,您就向项目中添加了notification service extension,并准备拦截一些推送通知。

3. Exposing Files to the Extension

首先,将项目中包含的一些helper类公开给您创建的新services extension。在Network目录下,你会找到两个文件:ImageDownloader.swiftNetworkError.swift

File inspector中,选中WendercastNotificationService target的复选框,这样它们就可以在services extension中使用:

4. Saving Files

WendercastNotificationService组中,打开NotificationService.swift并在文件的顶部导入UIKit

import UIKit

NotificationService的底部,添加这个方便的方法来保存一个图像到磁盘:

private func saveImageAttachment(
  image: UIImage,
  forIdentifier identifier: String
) -> URL? {
  // 1
  let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
  // 2
  let directoryPath = tempDirectory.appendingPathComponent(
    ProcessInfo.processInfo.globallyUniqueString,
    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
  }
}

以下是你所做的:

  • 1) 获取对temp文件目录的引用。
  • 2) 使用temp文件目录,使用惟一的字符串创建目录URL。
  • 3) FileManager负责创建用于存储数据的实际文件。调用createDirectory(at: winthintermediatedirectory:attributes:)创建一个空目录。
  • 4) 基于image identifier创建文件URL。
  • 5) 从映像创建一个Data对象。
  • 6) 尝试将文件写入磁盘。

现在您已经创建了一种存储图像的方法,接下来将把注意力转移到下载实际的图像上。

5. Downloading an Image

添加另一种方法从URL下载图像:

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

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

    // 4
    completion(image)
  }
}

它的作用很简单:

  • 1) 确保您可以从urlString属性创建URL
  • 2) 使用链接到此targetImageDownloader尝试下载。
  • 3) 确保生成的映像不是nil
  • 4) 调用completion块,传递UIImage结果。

Modifying the Push Content From the Server

您需要向发送到设备的push有效负载添加一些额外的值。进入Push Notification Tester应用,用这个payload替换body

{
  "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"
}

这个payload给推送通知一个title, a subtitle and a body

注意,mutable-content的值为1。该值告诉iOS内容是可更新的,从而在将其交付给用户之前调用service extension

添加了两个自定义键:podcast-imagepodcast-guest。在向用户显示通知之前,您将使用与这些键关联的值来更新推送内容。

现在发送带有以上内容的推送。您将看到一个更新的推送通知,其中添加了标题、副标题和描述。它是这样的:

1. Updating the Title

notification service extension的强大功能来自于它拦截推送的能力。在本节中,您将对此有所了解。在WendercastNotificationService中,打开NotificationService.swift并定位didReceive(_:withContentHandler:)。当推送通知出现时,将调用此函数,并允许您对将要显示给用户的内容执行一些调整。

if let块替换为:

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

  // 3
  contentHandler(bestAttemptContent)
}

以下是你所做的:

  • 1) 在通知内容notification contentuserInfo中检查键podcast-guest的值。
  • 2) 如果存在,请更新notification contenttitle
  • 3) 调用completion handler来交付推送。如果podcast-author的值不存在,push将显示原始标题。

构建和运行。然后将应用程序发送到后台。现在从push Notifications Tester应用程序发送一个推送。你会看到一个标题更新的推送通知,现在包含了podcast-author条目的值。

2. Adding an Image

接下来,您将使用push有效负载payload下载表示podcast的图像。

contentHandler(bestAttemptContent)替换为以下内容:

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

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

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

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

  // 7
  contentHandler(bestAttemptContent)
}

这是上面做的事情:

  • 1) 检查您是否有podcast-image的值。如果没有,则调用content handler来交付推送和返回。
  • 2) 调用便利方法来检索带有从push payload接收到的URL的图像。
  • 3) 当completion block触发时,检查图像是否为nil;否则,请尝试将其保存到磁盘。
  • 4) 如果有URL,则操作成功;如果这些检查失败,调用content handler并返回。
  • 5) 使用文件URL创建一个UNNotificationAttachment。将标识符图像命名为最终通知上的映像。
  • 6) 如果创建附件成功,则将其添加到bestAttemptContent上的attachments属性。
  • 7) 调用content handler来传递推送通知。

构建和运行。将app发送到后台。

现在,从Push Notifications Tester应用程序发送另一个带有相同负载的推送。你应该会看到推送的右上角的图片:

下拉通知。你会看到它会扩展了,并使用图像填充屏幕的大部分:

现在可以更新notification content了。接下来,您将进一步围绕推送内容创建自定义UI。


Creating a Custom UI

通过在推送内容的顶部添加一个自定义UI,您可以进一步使用富通知。这个界面将通过一个应用扩展来取代标准的推送通知UI。

这个界面是一个符合UNNotificationContentExtension的视图控制器(view controller)。通过实现didReceive(_:),您可以拦截通知并设置自定义界面。

1. Adding the Target

和之前一样,点击File New Target…,选择Notification Content Extension

content extension命名为WendercastNotificationContent,并确保区域正确(使用您自己的team and organization name):

这一次,单击schema activation confirmation屏幕上的Activate

2. Configuring Info.plist

接下来,您需要配置新targetInfo.plist信息,以显示您的content extension

  • 1) 在WendercastNotificationContent组中,打开Info.plist
  • 2) 扩展NSExtension dictionary字典。
  • 3) 扩展NSExtensionAttribute字典。
  • 4) 将UNNotificationExtensionCategory中的值更新为new_podcast_available
  • 5) 单击+NSExtensionAttribute字典添加一个新的键-值对。
  • 6) 将键UNNotificationExtensionDefaultContentHidden添加为布尔值,并将值设置为YES
  • 7) 再添加一个布尔键值对。将键设置为UNNotificationExtensionUserInteractionEnabled,将值设置为YES

你最后的Info.plist信息应该是这样的(键的顺序并不重要):

以下是这些参数的用途:

  • UNNotificationExtensionCategory

    • 该条目的值必须与传入推送内容中的值匹配。iOS需要这个来决定使用哪个UI来显示通知。
    • 您需要它,因为您可能希望为不同类别的推送通知提供自定义UI。
    • 如果该值丢失,iOS将不会调用您的扩展。
  • UNNotificationExtensionInitialContentSizeRatio

    • 这个值是一个介于01之间的数字。它表示自定义界面的宽高比。
    • 默认值1告诉iOS你的初始界面高度和它的宽度是一样的。
    • 例如,如果你将这个值设置为0.5,那么这将告诉iOS你的界面的高度是其宽度的一半。
    • 这是一个估计值,允许iOS设置界面的初始大小,防止不必要的调整。
  • UNNotificationExtensionDefaultContentHidden

    • 当设置为YES时,推送内容的标准title, subtitle and body是不可见的。
    • 当设置为NO时,标准推送内容将显示在自定义UI下。
      UNNotificationExtensionUserInteractionEnabled
    • 当设置为YES时,它允许用户与UIKit元素交互。

3. Adding the App Group

添加你为main app target创建的相同的app group

  • 1) 在文件导航器File navigator中,单击project节点。
  • 2) 选择WendercastNotificationContenttarget
  • 3) 选择Signing & Capabilities选项卡。
  • 4) 单击+ Capability
  • 5) 选择App Groups
  • 6) 选择您在本教程开始时创建的相同应用group ID

Building the Custom UI

您可能希望将重点放在构建content extension逻辑上,而不是将时间浪费在冗长的Interface Builder上。为了帮助您,使用已经包含的一个准备使用的故事板。您可以随意使用它来替换由Xcode自动创建的故事板。

以下是如何做到这一点:

  • 1) 在Xcode中,删除MainInterface.storyboard,来自WendercastNotificationContent组。选择Move to Trash提示。
  • 2) 从ContentStoryboard文件夹拖动MainInterface.storyboard文件到XcodeWendercastNotificationContent文件夹。
  • 3) 选中Copy items if needed,并在Add to targets列表中选择WendercastNotificationContenttarget
  • 4) 单击Finish
  • 5) 在Xcode中打开故事板。

您将看到这个故事板为notification UI提供了一个很好的起点(starting point)。它为title, podcast image, favorites button and play button提供UI元素。另外,自动布局约束已经设置好了。

现在你可以专注于构建视图控制器。


Setting up NotificationViewController

NotificationViewController负责为用户显示自定义通知视图(custom notification view)。你将做出必要的修改来呈现你的新推送通知视图。

1. Adding Shared Files

打开以下文件并将WendercastNotificationContent添加到它们在File inspector中的target membership中:

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

您可以通过选中WendercastNotificationContent框来做到这一点。

这将使数据模型和网络类对content extension可用。

注意:Xcode在这一点上可能有些混乱,显示了一些错误。您可以通过简单地按下Command-B键构建目标来清除它们。

2. Customizing the UI

NotificationViewController中查看类声明。删除由Xcode自动生成的代码:

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

然后在类声明下面添加以下输出:

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

然后添加一个属性来保存当前的podcast

var podcast: Podcast?

最后,添加以下方便的方法从共享data store加载podcast

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

  // 2
  guard let podcastLink = link else {
    return
  }

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

  // 4
  self.podcast = podcast
}

以下是你正在做的事情:

  • 1) 尝试从附加到通知的userInfo对象获取到podcast的链接。podcast链接是podcastCore Data存储中的唯一标识符。
  • 2) 如果链接不存在,请尽早返回。
  • 3) 使用该链接从Core Data存储获取Podcast模型对象。
  • 4) 设置一个带有响应的podcast

3. Customizing the Notification

didReceive(_:)body替换为:

// 1
loadPodcast(from: notification)

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

// 3
guard 
  let attachment = content.attachments.first,
  attachment.url.startAccessingSecurityScopedResource() 
  else {
    return
}

// 4
let fileURLString = attachment.url

guard 
  let imageData = try? Data(contentsOf: fileURLString),
  let image = UIImage(data: imageData) 
  else {
    attachment.url.stopAccessingSecurityScopedResource()
    return
}

// 5
imageView.image = image
attachment.url.stopAccessingSecurityScopedResource()

一旦推送通知来了,下面是你正在做的事情:

  • 1) 调用便利方法从Core Data存储加载podcast。这设置了以后使用的podcast
  • 2) 将title and body labels设置为从推送通知接收到的值。
  • 3) 尝试访问附加到service extension的媒体。如果没有,早点return。对startAccessingSecurityScopedResource()的调用允许您访问附件。
  • 4) 获取附件的URL。尝试从磁盘检索它并将数据转换为图像。如果失败了,早点return
  • 5) 如果图像检索成功,设置podcast图像并停止访问资源。

4. Implementing the Favorite Action

UI有一个按钮,用于将通知的podcast添加到收藏列表中。这是一个如何让推送通知变得可操作和酷的完美例子。

添加以下方法来处理点击收藏按钮:

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

  // 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
  CoreDataManager.shared.saveContext()
}

要处理收藏按钮点击,你是:

  • 1) 检查确保podcast已经设置好。
  • 2) 根据podcast切换isFavorite
  • 3) 更新收藏按钮UI以匹配模型状态。
  • 4) 使用更改更新Core Data存储。

这足以测试对content extension的第一组更改。将scheme设置回Wendercast,然后构建并运行,并将应用程序放到后台。接下来,从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"
}

一旦通知出现,就拉下它。你会看到你的更新自定义界面:

点击收藏(Favorite)按钮,你会看到它改变了状态。如果将Wendercast应用程序打开到同一个podcast,您会注意到收藏按钮的状态与notification UI的状态相匹配。太棒了!

5. Implementing the Play Action

现在你将实现一个深层链接到应用程序的播放动作。添加以下方法到NotificationViewController

@IBAction func playButtonTapped(_ sender: Any) {
  extensionContext?.performNotificationDefaultAction()
}

这告诉notification extension打开应用程序并以标准推送方式交付所有内容。

接下来,看看Wendercast/App/SceneDelegate.swift中的扩展。这段代码执行了很多相同的工作,你一直在做的扩展:

  • 寻找podcast link的存在。
  • 尝试从Core Data存储获取podcast
  • 告诉PodcastFeedTableViewController加载指定的podcast
  • 播放指定的podcast

构建和运行。将应用程序发送到后台,并推送与上次发送的相同的通知负载(payload)。这一次,点击播放按钮。该应用程序将会深入到podcast的细节,并开始播放该集。你已经做到了!

恭喜你!你已经深入研究了丰富的推送通知。你学会了如何:

  • 修改推送内容
  • 附加媒体
  • 创建自定义用户界面
  • 导航扩展和它们的主应用程序之间的交互

要了解更多,可以研究通知操作以及它们如何应用于通知内容扩展(notification content extensions)。通过启用可定制的UI和用户交互(customizable UI and user interaction),各种可能性实际上是无穷无尽的!首先,看看官方文件:

  • UserNotifications
  • UserNotificationsUI

后记

本篇主要讲述了APNs从工程配置到自定义通知UI全流程解析,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(系统推送的集成(十七) —— APNs从工程配置到自定义通知UI全流程解析(一))