教程:使用通知中心 REST API 向 Swift iOS 应用发送推送通知Tutorial: Send push notifications to Swift iOS apps using Notification Hubs REST APIs

本教程介绍如何使用 Azure 通知中心和 REST API 向 iOS 应用程序发送推送通知。This tutorial describes how to use Azure Notification Hubs to send push notifications to an iOS application using REST APIs.

本教程涵盖以下步骤:This tutorial covers the following steps:

  • 创建示例 iOS 应用。Create a sample iOS app.
  • 将 iOS 应用连接到 Azure 通知中心。Connect your iOS app to Azure Notification Hubs.
  • 发送测试推送通知。Send test push notifications.
  • 验证应用是否可以接收通知。Verify that your app receives notifications.

可以从 GitHub 下载本教程的完整代码。The complete code for this tutorial can be downloaded from GitHub.

先决条件Prerequisites

若要完成本教程,需要具备以下先决条件:To complete this tutorial, you need the following prerequisites:

  • 运行 Xcode 的 Mac,以及安装到密钥链中的有效开发人员证书。A Mac running Xcode, along with a valid developer certificate installed into your Keychain.

  • 运行 iOS 版本 10 或更高版本的 iPhone 或 iPad。An iPhone or iPad running iOS version 10 or later.

  • Apple 门户中注册并与证书关联的物理设备。 Your physical device registered in the Apple Portal and associated with your certificate.

在继续操作之前,请务必完成上一篇教程,了解如何开始使用适用于 iOS 应用的 Azure 通知中心,并在通知中心设置和配置推送凭据。Before you proceed, be sure to go through the previous tutorial on getting started with Azure Notification Hubs for iOS apps to set up and configure push credentials in your notification hub. 即使你没有 iOS 开发经验,也可以按照这些步骤操作。Even if you have no prior experience with iOS development, you should be able to follow these steps.

备注

由于推送通知的配置要求,必须在物理 iOS 设备(iPhone 或 iPad)而不是在 iOS 仿真器上部署和测试推送通知。Because of configuration requirements for push notifications, you must deploy and test push notifications on a physical iOS device (iPhone or iPad), instead of the iOS emulator.

在以下部分中,你将生成一个连接到通知中心的 iOS 应用。In the following sections, you build an iOS app that connects to the notification hub.

创建 iOS 项目Create an iOS project

  1. 在 Xcode 中,创建新的 iOS 项目,并选择“单视图应用程序”模板。 ****  In Xcode, create a new iOS project and select the Single View Application template.

  2. 设置新项目的选项:When setting the options for the new project:

    • 指定在 Apple 开发人员门户中设置“捆绑标识符”时使用的“产品名称”(PushDemo) 和“组织标识符”(com.<organization>)。 ****   ****   Specify the Product Name (PushDemo) and Organization Identifier (com.<organization>) that you used when you set the Bundle Identifier in the Apple Developer Portal.
    • 选择为其设置了“应用 ID”的“团队”。 ****   ****  Choose the Team for which the App ID was set up.
    • 将“语言”设置为“Swift”。 ****   ****Set the language to Swift.
    • 选择“下一步”。 ****Select Next.
  3. 创建名为 SupportingFiles 的新文件夹。 ****Create a new folder called SupportingFiles.

  4. 在 SupportingFiles 文件夹中创建名为 devsettings.plist 的新属性表文件。 ****   ****  Create a new p-list file called devsettings.plist in the SupportingFiles folder. 请确保将此文件夹添加到 gitignore 文件,以便在使用 git 存储库时不会提交该文件。 ****  Be sure to add this folder to your gitignore file so it isn't committed when working with a git repo. 在生产应用中,可能会在自动生成过程中有条件地设置这些机密。In a production app, you might be conditionally setting these secrets as part of an automated build process. 本教程未介绍这些设置。These settings are not covered in this tutorial.

  5. 使用自己在通知中心预配的值更新 devsettings.plist,以在该文件中包含以下配置条目: ****  Update devsettings.plist to include the following configuration entries by using your own values from the notification hub that you provisioned:

    KeyKey 类型Type Value
    notificationHubKeynotificationHubKey 字符串String <hubKey>
    notificationHubKeyNamenotificationHubKeyName 字符串String <hubKeyName>
    notificationHubNamenotificationHubName 字符串String <hubName>
    notificationHubNamespacenotificationHubNamespace 字符串String <hubNamespace>

    可以通过导航到 Azure 门户中的通知中心资源来查找所需的值。You can find the required values by navigating to the notification hub resource in the Azure portal. 具体而言,“notificationHubName”和“notificationHubNamespace”值都位于“概述”页面中“基本要素”摘要的右上角。 ****   ****   ****   ****  In particular, the notificationHubName and notificationHubNamespace values are in the upper-right corner of the Essentials summary within the Overview page.

    所需值

    此外,还可以通过导航到“访问策略”并选择相应的“访问策略”(如“DefaultFullSharedAccessSignature”)来查找“notificationHubKeyName”和“notificationHubKey”值。 ****   ****   ****   ****  ****You can also find the notificationHubKeyName and notificationHubKey values by navigating to Access Policies and selecting the respective Access Policy, such as DefaultFullSharedAccessSignature. 然后,从“主连接字符串”复制前缀为 SharedAccessKeyName= 的“notificationHubKeyName”值,以及前缀为 SharedAccessKey= 的“notificationHubKey”值。 ****    ****   ****After that, copy from the Primary Connection String the value prefixed with SharedAccessKeyName= for notificationHubKeyName, and the value prefixed with SharedAccessKey= for the notificationHubKey. 连接字符串应为以下格式:The connection string should be in the following format:

    Endpoint=sb://<namespace>.servicebus.chinacloudapi.cn/;SharedAccessKeyName=<notificationHubKeyName>;SharedAccessKey=<notificationHubKey>
    

    为简单起见,请指定“DefaultFullSharedAccessSignature”,以便使用令牌来发送通知。 ****For simplicity, specify DefaultFullSharedAccessSignature, so that you can use the token to send notifications. 在实践中,对于只想接收通知的情况,最好选择“DefaultListenSharedAccessSignature”。 ****  In practice, the DefaultListenSharedAccessSignature is a better choice for situations in which you only want to receive notifications.

  6. 在“项目导航器”下,选择“项目名称”,然后选择“常规”选项卡。 ****  ****   ****  Under Project Navigator, select the Project Name and then select the General tab.

  7. 查找“标识”,然后设置“捆绑标识符”值,使其与 com.<organization>.PushDemo(上一步骤中用于“应用 ID”的值)匹配。 ****   ****   ****  Find Identity and then set the Bundle Identifier value so that it matches com.<organization>.PushDemo, which is the value used for the App ID from a previous step.

  8. 查找“签名和功能”,然后为“Apple 开发人员帐户”选择适当的“团队”。 ****  ****   ****Find Signing & Capabilities, and then select the appropriate Team for your Apple Developer Account. “团队”值应与创建证书和配置文件时所用的值匹配。 ****  The Team value should match the one under which you created your certificates and profiles.

  9. Xcode 应根据“捆绑标识符”自动下载适当的“预配配置文件”值。 ****   ****Xcode should automatically download the appropriate Provisioning Profile value based on the Bundle Identifier. 如果未显示新的“预配配置文件”值,请尝试通过依次选择“Xcode”、“首选项”、“帐户”来刷新“签名标识”的配置文件,然后选择“下载手动配置文件”按钮来下载配置文件。 ****   ****   ****  ****  ****  ****  If you don't see the new Provisioning Profile value, try refreshing the profiles for the Signing Identity by selecting Xcode, then PreferencesAccount, and then select the Download Manual Profiles button to download the profiles.

  10. 仍在“签名和功能”选项卡上,单击“+ 功能”按钮,然后双击列表中的“推送通知”,以确保启用“推送通知”。 ****   ****   ****   ****  Still on the Signing & Capabilities tab, click the + Capability button and double tap on Push Notifications from the list to ensure Push Notifications are enabled.

  11. 打开 AppDelegate.swift 文件以实现“UNUserNotificationCenterDelegate”协议,并将以下代码添加到类的顶部: ****   ****  Open your AppDelegate.swift file to implement the UNUserNotificationCenterDelegate protocol and add the following code to the top of the class:

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    
       ...
    
       var configValues: NSDictionary?
       var notificationHubNamespace : String?
       var notificationHubName : String?
       var notificationHubKeyName : String?
       var notificationHubKey : String?
       let tags = ["12345"]
    
       ...
    
    }
    

    稍后将使用这些成员。You'll use these members later. 具体而言,将在使用“自定义模板”进行注册的过程中使用“标记”成员。 ****   ****Specifically, you'll use the tags member as part of the registration using a custom template. 有关标记的详细信息,请参阅 注册标记 和 模板注册For more information on tags, see Tags for registrations and Template registrations.

  12. 在同一文件中,将以下代码添加到 didFinishLaunchingWithOptions 函数:In the same file, add the following code to the didFinishLaunchingWithOptions function:

    if let path = Bundle.main.path(forResource: "devsettings", ofType: "plist") {
       if let configValues = NSDictionary(contentsOfFile: path) {
           self.notificationHubNamespace = configValues["notificationHubNamespace"] as? String
           self.notificationHubName = configValues["notificationHubName"] as? String
           self.notificationHubKeyName = configValues["notificationHubKeyName"] as? String
           self.notificationHubKey = configValues["notificationHubKey"] as? String
       }
    }
    
    if #available(iOS 10.0, *){
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
            (granted, error) in
    
            if (granted)
            {
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            }
        }
    }
    
    return true
    

    此代码可检索“devsettings.plist”中的设置,将“AppDelegate”类设置为“UNUserNotificationCenter”委托,请求推送通知的授权,然后调用“registerForRemoteNotifications”。 ****  ****   ****   ****This code retrieves the settings from devsettings.plist, sets the AppDelegate class as the UNUserNotificationCenter delegate, requests authorization for push notifications, and then calls registerForRemoteNotifications.

    为简单起见,该代码仅支持 iOS 10 和更高版本。For simplicity, the code only supports iOS 10 and later. 可以像往常一样,通过有条件地使用相应的 API 和方法来添加对以前的 iOS 版本的支持。You can add support for previous iOS versions by conditionally using the respective APIs and approaches, as you would normally do.

  13. 在同一文件中添加以下代码:In the same file, add the following code:

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let installationId = (UIDevice.current.identifierForVendor?.description)!
        let pushChannel = deviceToken.reduce("", {$0 + String(format: "%02X", $1)})
    }
    
    func showAlert(withText text : String) {
        let alertController = UIAlertController(title: "PushDemo", message: text, preferredStyle: UIAlertControllerStyle.alert)
        alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default,handler: nil))
        let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
        keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
    }
    

    此代码使用“installationId”和“pushChannel”值向通知中心进行注册。 ****   ****  This code uses the installationId and pushChannel values to register with the notification hub. 在这种情况下,将使用“UIDevice.current.identifierForVendor”来提供用于标识设备的唯一值,然后将“deviceToken”的格式设置为提供所需的“pushChannel”值。 ****   ****   ****  In this case, you're using UIDevice.current.identifierForVendor to provide a unique value to identify the device and then formatting the deviceToken to provide the desired pushChannel value. showAlert 函数的存在只是为了出于演示目的显示某些消息文本。 ****  The showAlert function exists simply to display some message text for demonstration purposes.

  14. 仍在 AppDelegate.swift 文件中,将 willPresent 和 didReceive 函数添加到“UNUserNotificationCenterDelegate”。 ****   ****   ****   ****Still in the AppDelegate.swift file, add the willPresent and didReceive functions to UNUserNotificationCenterDelegate. 这些函数会在收到应用正在其前台或后台运行的通知时显示警报。These functions display an alert when they're notified that an app is running in either the foreground or the background.

    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        showAlert(withText: notification.request.content.body)
    }
    
    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void) {
        showAlert(withText: response.notification.request.content.body)
    }
    
  15. print 语句添加到 didRegisterForRemoteNotificationsWithDeviceToken 函数的底部,验证是否已为“installationId”和“pushChannel”分配值: ****   ****   ****  Add print statements to the bottom of the didRegisterForRemoteNotificationsWithDeviceToken function to verify that installationId and pushChannel are being assigned values:

    print(installationId)
    print(pushChannel)
    
  16. 为稍后要添加到项目的基础组件创建“模型”、“服务”和“实用程序”文件夹。 ****  ****  ****  Create the ModelsServices, and Utilities folders for the foundational components you'll be adding to the project later.

  17. 检查项目是否在物理设备上生成和运行。Check that the project builds and runs on a physical device. 不能使用模拟器测试推送通知。Push notifications cannot be tested by using the simulator.

创建模型Create models

在此步骤中,将创建一组模型来表示 通知中心 REST API 有效负载,并存储所需的共享访问签名 (SAS) 令牌数据。In this step, you create a set of models to represent the Notification Hubs REST API payloads, and to store the required shared access signature (SAS) token data.

  1. 将名为 PushTemplate.swift 的新 Swift 文件添加到“模型”文件夹中。 ****   ****  Add a new Swift file called PushTemplate.swift to the Models folder. 此模型定义一个结构,该结构表示“DeviceInstallation”有效负载中单个模板的正文。 ****   ****  This model defines a structure that represents the BODY of an individual template as part of the DeviceInstallation payload.

    import Foundation
    
    struct PushTemplate : Codable {
        let body : String
    
        init(withBody body : String) {
            self.body = body
        }
    }
    
  2. 将名为 DeviceInstallation.swift 的新 Swift 文件添加到“模型”文件夹中。 ****   ****  Add a new Swift file called DeviceInstallation.swift to the Models folder. 此文件定义一个结构,该结构表示用于创建或更新“设备安装”的有效负载。 ****This file defines a structure that represents the payload for creating or updating a Device Installation. 将以下代码添加到该文件:Add the following code to the file:

    import Foundation
    
    struct DeviceInstallation : Codable {
        let installationId : String
        let pushChannel : String
        let platform : String = "apns"
        var tags : [String]
        var templates : Dictionary<String, PushTemplate>
    
        init(withInstallationId installationId : String, andPushChannel pushChannel : String) {
            self.installationId = installationId
            self.pushChannel = pushChannel
            self.tags = [String]()
            self.templates = Dictionary<String, PushTemplate>()
        }
    }
    
  3. 将名为 TokenData.swift 的新 Swift 文件添加到“模型”文件夹中。 ****   ****  Add a new Swift file called TokenData.swift to the Models folder. 此模型用于存储 SAS 令牌及其过期时间。This model is used to store a SAS token along with its expiration. 将以下代码添加到该文件:Add the following code to the file:

    import Foundation
    
    struct TokenData {
    
        let token : String
        let expiration : Int
    
        init(withToken token : String, andTokenExpiration expiration : Int) {
            self.token = token
            self.expiration = expiration
        }
    }
    

生成 SAS 令牌Generate a SAS token

通知中心使用与 Azure 服务总线相同的安全基础结构。Notification Hubs uses the same security infrastructure as Azure Service Bus. 要调用 REST API,请以编程方式生成 SAS 令牌,可在请求的“授权”标头中使用该令牌。  ****  To call the REST API, programmatically generate a SAS token that can be used in the Authorization header of the request.

生成的令牌格式如下:The resulting token will be in the following format:

SharedAccessSignature sig=\<UrlEncodedSignature\>\&se=\<ExpiryEpoch\>\&skn=\<KeyName\>\&sr=\<UrlEncodedResourceUri\> |

此过程本身涉及相同的六个步骤:The process itself involves the same six steps:

  1. 以 UNIX 纪元时间格式计算过期时间,表示自 1970 年 1 月 1 日午夜(协调世界时)以来所经过的秒数。Compute the expiry in UNIX Epoch time format, which means the number of seconds elapsed since midnight Universal Coordinated Time, January 1, 1970.

  2. 设置“ResourceUrl”(表示要尝试访问的资源)的格式,使其为百分比编码和小写格式。 ****  Format the ResourceUrl that represents the resource you're trying to access so it's percent-encoded and lowercase. ResourceUrl 的格式为https://<namespace>.servicebus.chinacloudapi.cn/<hubName>。 ****  The ResourceUrl has the format https://<namespace>.servicebus.chinacloudapi.cn/<hubName>.

  3. 准备“StringToSign”,将其格式设置为<UrlEncodedResourceUrl>\n<ExpiryEpoch>。 ****Prepare the StringToSign, which is formatted as <UrlEncodedResourceUrl>\n<ExpiryEpoch>.

  4. 通过使用“StringToSign”值的 HMAC-SHA256 哈希,计算“签名”并对其进行 Base64 编码。 ****   ****  Compute and Base64-encode the Signature by using the HMAC-SHA256 hash of the StringToSign value. 哈希值与相应“授权规则”的“连接字符串”的“密钥”部分一起使用。 ****   ****   ****The hash value is used with the Key part of the Connection String for the respective Authorization Rule.

  5. 设置 Base64 编码的“签名”的格式,使其为百分比编码格式。 ****  Format the Base64-encoded Signature so it's percent encoded.

  6. 使用“UrlEncodedSignature”、“ExpiryEpoch”、“KeyName”和“UrlEncodedResourceUrl”值,以预期的格式来构造令牌。 ****  ****  ****  ****  Construct the token in the expected format by using the UrlEncodedSignatureExpiryEpochKeyName, and UrlEncodedResourceUrl values.

有关共享访问签名以及 Azure 服务总线和通知中心如何使用它的更完整概述,请参阅 Azure 服务总线文档。 See the Azure Service Bus documentation for a more complete overview of shared access signatures, and how Azure Service Bus and Notification Hubs use it.

对于此 Swift 示例,请使用 Apple 开源 CommonCrypto 库来帮助对签名进行哈希处理。 ****  For the purposes of this Swift example, you use the Apple open-source CommonCrypto library to help with the hashing of the signature. 由于它是 C 库,因此无法在现有的 Swift 中访问。As it's a C library, it is not accessible in Swift out of the box. 可以通过使用桥接标头来使该库可用。You can make the library available by using a bridging header.

添加和配置桥接标头:To add and configure the bridging header:

  1. 在 Xcode 中,依次选择“文件”、“新建”、“文件”和“头文件”。 ****  ****  ****  ****In Xcode, select File, then New, then File, and then select Header File. 将头文件命名为 BridgingHeader.h。 ****Name the header file BridgingHeader.h.

  2. 编辑该文件以导入 CommonHMAC.h: ****Edit the file to import CommonHMAC.h:

    #import <CommonCrypto/CommonHMAC.h>
    
    #ifndef BridgingHeader_h
    #define BridgingHeader_h
    
    #endif /* BridgingHeader_h */
    
  3. 更新目标“生成设置”以引用桥接标头: ****  Update the target Build Settings to reference the bridging header:

    1. 选择“PushDemo”项目,然后向下滚动到“Swift 编译器”部分。 ****   ****  Select the PushDemo project and scroll down to the Swift Compiler section.

    2. 确保“安装 Objective-C 兼容性标头”选项设置为“是”。 ****   ****Ensure that the Install Objective-C Compatibility Header option is set to Yes.

    3. 在“Objective-C 桥接标头”选项中输入文件路径 <ProjectName>/BridgingHeader.h。  ****  Enter the file path <ProjectName>/BridgingHeader.h in the Objective-C bridging Header option. 这是桥接标头的文件路径。This is the file path to the bridging header.

    如果找不到这些选项,请确保已选择“全部”视图,而不是“基本”或“自定义”视图。 ****   ****   ****If you can't find these options, ensure that you have the All view selected, rather than Basic or Customized.

    借助许多可用的第三方开源包装器库,可更轻松地使用 CommonCrypto。 ****  There are many third-party open-source wrapper libraries available that might make using CommonCrypto a bit easier. 但是,这些库不在本文的讨论范围之内。However, discussion of these libraries is beyond the scope of this article.

  4. 在“实用程序”文件夹中添加一个名为 TokenUtility.swift 的新 Swift 文件,并添加以下代码: ****   ****  Add a new Swift file named TokenUtility.swift within the Utilities folder, and add the following code:

    import Foundation
    
    struct TokenUtility {
       typealias Context = UnsafeMutablePointer<CCHmacContext>
    
       static func getSasToken(forResourceUrl resourceUrl : String, withKeyName keyName : String, andKey key : String, andExpiryInSeconds expiryInSeconds : Int = 3600) -> TokenData {
         let expiry = (Int(NSDate().timeIntervalSince1970) + expiryInSeconds).description
         let encodedUrl = urlEncodedString(withString: resourceUrl)
         let stringToSign = "\(encodedUrl)\n\(expiry)"
         let hashValue = sha256HMac(withData: stringToSign.data(using: .utf8)!, andKey: key.data(using: .utf8)!)
         let signature = hashValue.base64EncodedString(options: .init(rawValue: 0))
         let encodedSignature = urlEncodedString(withString: signature)
         let sasToken = "SharedAccessSignature sr=\(encodedUrl)&sig=\(encodedSignature)&se=\(expiry)&skn=\(keyName)"
         let tokenData = TokenData(withToken: sasToken, andTokenExpiration: expiryInSeconds)
    
         return tokenData
       }
    
       private static func sha256HMac(withData data : Data, andKey key : Data) -> Data {
          let context = Context.allocate(capacity: 1)
          CCHmacInit(context, CCHmacAlgorithm(kCCHmacAlgSHA256), (key as NSData).bytes, size_t((key as NSData).length))
          CCHmacUpdate(context, (data as NSData).bytes, (data as NSData).length)
          var hmac = Array<UInt8>(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
          CCHmacFinal(context, &hmac)
    
          let result = NSData(bytes: hmac, length: hmac.count)
          context.deallocate()
    
          return result as Data
       }
    
       private static func urlEncodedString(withString stringToConvert : String) -> String {
          var encodedString = ""
          let sourceUtf8 = (stringToConvert as NSString).utf8String
          let length = strlen(sourceUtf8)
    
          let charArray: [Character] = [ ".", "-", "_", "~", "a", "z", "A", "Z", "0", "9"]
          let asUInt8Array = String(charArray).utf8.map{ Int8($0) }
    
          for i in 0..<length {
              let currentChar = sourceUtf8![i]
    
              if (currentChar == asUInt8Array[0] || currentChar == asUInt8Array[1] || currentChar == asUInt8Array[2] || currentChar == asUInt8Array[3] ||
                 (currentChar >= asUInt8Array[4] && currentChar <= asUInt8Array[5]) ||
                 (currentChar >= asUInt8Array[6] && currentChar <= asUInt8Array[7]) ||
                 (currentChar >= asUInt8Array[8] && currentChar <= asUInt8Array[9])) {
                  encodedString += String(format:"%c", currentChar)
              }
              else {
                  encodedString += String(format:"%%%02x", currentChar)
              }
          }
    
          return encodedString
       }
    }
    

    此实用程序可封装用于生成 SAS 令牌的逻辑。This utility encapsulates the logic responsible for generating the SAS token.

    如前文所述,getSasToken 函数可协调准备令牌所需的高级步骤。 ****  As outlined previously, the getSasToken function orchestrates the high-level steps required to prepare the token. 本教程稍后部分介绍的安装服务将调用该函数。The function will be called by the installation service later in this tutorial.

    getSasToken 函数将调用其他两个函数:sha256HMac 用于计算签名,urlEncodedString 用于对关联的 URL 字符串进行编码。 ****   ****   ****  The other two functions are called by the getSasToken function: sha256HMac for computing the signature, and urlEncodedString for encoding the associated URL string. urlEncodedString 函数是必需的,因为无法通过使用内置的 addingPercentEncoding 函数来实现所需输出。 ****   ****  The urlEncodedString function is required, as it is not possible to achieve the required output by using the built-in addingPercentEncoding function.

     Azure 存储 iOS SDK 很好地演示了如何在 Objective-C 中进行这些操作。The Azure Storage iOS SDK is an excellent example of how to approach these operations in Objective-C. 有关 Azure 服务总线 SAS 令牌的详细信息,请参阅 Azure 服务总线文档Further information on Azure Service Bus SAS tokens can be found in the Azure Service Bus documentation.

  5. 在 AppDelegate.swift 中,将以下代码添加到 didRegisterForRemoteNotificationsWithDeviceToken 函数中,以验证“TokenUtility.getSasToken”是否在生成有效令牌 ****  ****  In AppDelegate.swift, add the following code to the didRegisterForRemoteNotificationsWithDeviceToken function to verify that the TokenUtility.getSasToken is generating a valid token

    let baseAddress = "https://<notificaitonHubNamespace>.servicebus.chinacloudapi.cn/<notifiationHubName>"
    
    let tokenData = TokenUtility.getSasToken(forResourceUrl: baseAddress,
                                             withKeyName: self.notificationHubKeyName!,
                                             andKey: self.notificationHubKey!)
    
    print(tokenData.token)
    

    请务必将“baseAddress”字符串中的占位符替换为自己的值。 ****  Be sure to replace the placeholders in the baseAddress string with your own values.

验证 SAS 令牌Verify the SAS token

在客户端中实现安装服务之前,请使用 HTTP 实用工具检查应用是否在正确生成 SAS 令牌。Before you implement the installation service in the client, check that your app is correctly generating the SAS token by using an HTTP utility. 对于本教程,我们选择的工具是 Postman。 ****For the purposes of this tutorial, our tool of choice is Postman.

记下该应用生成的“installationId”和“令牌”值。 ****   ****  Make a note of the installationId and token values generated by the app.

按照以下步骤调用安装 API: ****  Follow these steps to call the installations API:

  1. 在 Postman 中,打开一个新选项卡。 ****In Postman, open a new tab.

  2. 将请求设置为“GET”并指定以下地址: ****  Set the request to GET and specify the following address:

    https://<namespace>.servicebus.chinacloudapi.cn/<hubName>/installations/<installationId>?api-version=2015-01
    
  3. 配置请求标头,如下所示:Configure the request headers as follows:

    KeyKey Value
    Content-TypeContent-Type application/jsonapplication/json
    授权Authorization <sasToken>
    x-ms-versionx-ms-version 2015-012015-01
  4. 选择右上角的“保存”按钮下显示的“代码”按钮。 ****   ****  Select the Code button that appears on the upper-right under the Save button. 请求应类似于以下示例:The request should look similar to the following example:

    GET /<hubName>/installations/<installationId>?api-version=2015-01 HTTP/1.1
    Host: <namespace>.servicebus.chinacloudapi.cn
    Content-Type: application/json
    Authorization: <sasToken>
    x-ms-version: 2015-01
    Cache-Control: no-cache
    Postman-Token: <postmanToken>
    
  5. 选择“发送”按钮。 ****  Select the Send button.

此时,指定的“installationId”尚无任何注册。 ****  No registration exists for the specified installationId at this point. 验证应导致“404 未找到”响应,而不是“401 未授权”响应。The verification should result in a "404 Not Found" response rather than a "401 Unauthorized" response. 此结果应确认已接受 SAS 令牌。This result should confirm that the SAS token has been accepted.

实现安装服务类Implement the installation service class

接下来,实现围绕 安装 REST API 的基本包装器。Next, implement a basic wrapper around the Installations REST API.

在“服务”文件夹下,添加一个名为 NotificationRegistrationService.swift 的新 Swift 文件,然后将以下代码添加到此文件: ****   ****  Add a new Swift file named NotificationRegistrationService.swift under the Services folder, and then add the following code to this file:

import Foundation

class NotificationRegistrationService {
    private let tokenizedBaseAddress: String = "https://%@.servicebus.chinacloudapi.cn/%@"
    private let tokenizedCreateOrUpdateInstallationRequest = "/installations/%@?api-version=%@"
    private let session = URLSession(configuration: URLSessionConfiguration.default)
    private let apiVersion = "2015-01"
    private let jsonEncoder = JSONEncoder()
    private let defaultHeaders: [String : String]
    private let installationId : String
    private let pushChannel : String
    private let hubNamespace : String
    private let hubName : String
    private let keyName : String
    private let key : String
    private var tokenData : TokenData? = nil
    private var tokenExpiryDate : Date? = nil

    init(withInstallationId installationId : String,
            andPushChannel pushChannel : String,
            andHubNamespace hubNamespace : String,
            andHubName hubName : String,
            andKeyName keyName : String,
            andKey key: String) {
        self.installationId = installationId
        self.pushChannel = pushChannel
        self.hubNamespace = hubNamespace
        self.hubName = hubName
        self.keyName = keyName
        self.key = key
        self.defaultHeaders = ["Content-Type": "application/json", "x-ms-version": apiVersion]
    }

    func register(
        withTags tags : [String]? = nil,
        andTemplates templates : Dictionary<String, PushTemplate>? = nil,
        completeWith completion: ((_ result: Bool) -> ())? = nil) {

        var deviceInstallation = DeviceInstallation(withInstallationId: installationId, andPushChannel: pushChannel)

        if let tags = tags {
            deviceInstallation.tags = tags
        }

        if let templates = templates {
            deviceInstallation.templates = templates
        }

        if let deviceInstallationJson = encodeToJson(deviceInstallation) {
            let sasToken = getSasToken()
            let requestUrl = String.init(format: tokenizedCreateOrUpdateInstallationRequest, installationId, apiVersion)
            let apiEndpoint = "\(getBaseAddress())\(requestUrl)"

            var request = URLRequest(url: URL(string: apiEndpoint)!)
            request.httpMethod = "PUT"

            for (key,value) in self.defaultHeaders {
                request.addValue(value, forHTTPHeaderField: key)
            }

            request.addValue(sasToken, forHTTPHeaderField: "Authorization")
            request.httpBody = Data(deviceInstallationJson.utf8)

            (self.session.dataTask(with: request) { dat, res, err in
                if let completion = completion {
                        completion(err == nil &&
                        (res as! HTTPURLResponse).statusCode == 200)
                }
            }).resume()
        }
    }

    private func getBaseAddress() -> String {
        return String.init(format: tokenizedBaseAddress, hubNamespace, hubName)
    }

    private func getSasToken() -> String {
        if (tokenData == nil ||
            tokenExpiryDate == nil ||
            Date() >= tokenExpiryDate!) {

            self.tokenData = TokenUtility.getSasToken(
                forResourceUrl: getBaseAddress(),
                withKeyName: self.keyName,
                andKey: self.key)

            self.tokenExpiryDate = Date(timeIntervalSinceNow: -(5 * 60))
                .addingTimeInterval(TimeInterval(tokenData!.expiration))
        }

        return (tokenData?.token)!
    }

    private func encodeToJson<T : Encodable>(_ object: T) -> String? {
        do {
            let jsonData = try jsonEncoder.encode(object)
            if let jsonString = String(data: jsonData, encoding: .utf8) {
                return jsonString
            } else {
                return nil
            }
        }
        catch {
            return nil
        }
    }
}

初始化过程中提供了必要的详细信息。The requisite details are provided as part of initialization. 可以选择将标记和模板传递到 register 函数,以构成“设备安装”JSON 有效负载的一部分。 ****   ****  Tags and templates are optionally passed into the register function to form part of the Device Installation JSON payload.

register 函数会调用其他专用函数来准备请求。 ****  The register function calls the other private functions to prepare the request. 收到响应后,将调用 completion,并指示注册是否成功。After a response is received, the completion is called and indicates whether the registration was successful.

请求终结点由 getBaseAddress 函数构造。 ****  The request endpoint is constructed by the getBaseAddress function. 该构造使用在初始化期间提供的通知中心参数“namespace”和“name”。 **   **  The construction uses the notification hub parameters namespace and name that were provided during initialization.

getSasToken 函数可检查当前存储的令牌是否有效。 ****  The getSasToken function checks whether the currently stored token is valid. 如果令牌无效,则该函数将调用 TokenUtility来生成新令牌,然后先存储再返回一个值。 ****  If the token isn't valid, the function calls TokenUtility to generate a new token and then stores it before returning a value.

最后,encodeToJson 将各个模型对象转换为 JSON,以用作请求正文的一部分。 ****Finally, encodeToJson converts the respective model objects into JSON for use as part of the request body.

调用通知中心 REST APIInvoke the Notification Hubs REST API

最后一步是更新“AppDelegate”,以使用“NotificationRegistrationService”向“NotificationHub”进行注册。 ****   ****   ****The last step is updating AppDelegate to use NotificationRegistrationService to register with the NotificationHub.

  1. 打开“AppDelegate.swift”并添加类级别的变量,以存储对“NoficiationRegistrationService”和通用“PushTemplate”的引用: ****   ****   ****Open AppDelegate.swift and add class-level variables to store a reference to the NoficiationRegistrationService and the generic PushTemplate:

    var registrationService : NotificationRegistrationService?
    let genericTemplate = PushTemplate(withBody: "{\"aps\":{\"alert\":\"$(message)\"}}")
    
  2. 在同一文件中,更新 didRegisterForRemoteNotificationsWithDeviceToken 函数以使用必要的参数初始化“NotificationRegistrationService”,然后调用 register 函数。 ****   ****   ****  In the same file, update the didRegisterForRemoteNotificationsWithDeviceToken function to initialize the NotificationRegistrationService with the requisite parameters, and then call the register function.

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
     let installationId = (UIDevice.current.identifierForVendor?.description)!
     let pushChannel = deviceToken.reduce("", {$0 + String(format: "%02X", $1)})
    
     // Initialize the Notification Registration Service
     self.registrationService = NotificationRegistrationService(
         withInstallationId: installationId,
         andPushChannel: pushChannel,
         andHubNamespace: notificationHubNamespace!,
         andHubName: notificationHubName!,
         andKeyName: notificationHubKeyName!,
         andKey: notificationHubKey!)
    
     // Call register, passing in the tags and template parameters
     self.registrationService!.register(withTags: tags, andTemplates: ["genericTemplate" : self.genericTemplate]) { (result) in
         if !result {
             print("Registration issue")
         } else {
             print("Registered")
           }
        }
    }
    

    为简单起见,该代码使用 print 语句在输出窗口中更新“register”操作的结果。 ****  For simplicity, the code uses print statements to update the output window with the result of the register operation.

  3. 在物理设备上生成并运行应用。Build and run the app on a physical device. 输出窗口中应显示“已注册”。You should see Registered in the output window.

测试解决方案Test the solution

此时,应用已在“NotificationHub”中注册,并且可以接收推送通知。 ****  At this point the app is registered with NotificationHub and can receive push notifications. 在 Xcode 中,停止调试程序并关闭当前正在运行的应用。In Xcode, stop the debugger and close the app if it's currently running. 接下来,检查“设备安装”详细信息是否按预期显示,以及应用现在是否可以接收推送通知。 ****  Next, check that the Device Installation details appear as expected and that the app can now receive push notifications.

验证设备安装Verify the device installation

现在,可以使用 Postman 发出与以前相同的请求,以 验证 SAS 令牌。 ****  You can now make the same request as you did previously, by using Postman for verifying the SAS token. 假设 SAS 令牌尚未过期,则响应现在应包括所提供的安装详细信息,例如模板和标记。Assuming that the SAS token hasn't expired, the response should now include the installation details you provided, such as the templates and tags.

{
    "installationId": "<installationId>",
    "pushChannel": "<pushChannel>",
    "pushChannelExpired": false,
    "platform": "apns",
    "expirationTime": "9999-12-31T23:59:59.9999999Z",
    "tags": [
        "12345"
    ],
    "templates": {
        "genericTemplate": {
            "body": "{\"aps\":{\"alert\":\"$(message)\"}}",
            "tags": [
                "genericTemplate"
            ]
        }
    }
}

如果以前的 SAS 令牌已过期,则可以在“TokenUtility”类的第 24 行添加一个断点,以获取新的 SAS 令牌并使用该新值更新“授权”标头。 ****   ****  If your previous SAS token has expired, you can add a breakpoint to line 24 of the TokenUtility class to get a new SAS token and update the Authorization header with that new value.

发送测试通知(Azure 门户)Send a test notification (Azure portal)

测试现在是否可以接收通知的最快方法是浏览到 Azure 门户中的通知中心:The quickest way to test that you can now receive notifications is to browse to the notification hub in the Azure portal:

  1. 在 Azure 门户中,浏览到通知中心的“概述”选项卡。 ****  In the Azure portal, browse to the Overview tab on your notification hub.

  2. 选择“测试发送”,它位于门户窗口左上方的“基本要素”摘要上方: ****  ****  Select Test Send, which is above the Essentials summary in the upper-left of the portal window:

    通知中心基本要素摘要测试发送

  3. 从“平台”列表中选择“自定义模板”。 ****   ****  Choose Custom Template from the Platforms list.

  4. 对于“发送到标记表达式”,输入“12345”。 ****   ****Enter 12345 for the Send to Tag Expression. 先前已在安装中指定了此标记。You had previously specified this tag in your installation.

  5. (可选)在 JSON 有效负载中编辑“消息”: ****  Optionally, edit the message in the JSON payload:

    通知中心测试发送

  6. 选择“发送” **** 。Select Send. 门户应指示通知是否已成功发送到设备:The portal should indicate whether the notification was successfully sent to the device:

    测试发送结果

    假设应用未在前台运行,则还应在设备的“通知中心”看到一条通知。 ****  Assuming that the app isn't running in the foreground, you should also see a notification in the Notification Center on your device. 点击通知应打开应用并显示警报。Tapping the notification should open the app and show the alert.

    测试通知

发送测试通知(邮件运营商)Send a test notification (mail carrier)

可以使用 Postman 通过 REST API 来发送通知,这可能是一种更方便的测试方法。 ****You can send notifications via the REST API using Postman, which may be a more convenient way to test.

  1. 在 Postman 中打开一个新选项卡。 ****Open a new tab in Postman.

  2. 将请求设置为“POST”,然后输入以下地址: ****Set the request to POST, and enter the following address:

    https://<namespace>.servicebus.chinacloudapi.cn/<hubName>/messages/?api-version=2015-01
    
  3. 配置请求标头,如下所示:Configure the request headers as follows:

    密钥Key Value
    Content-TypeContent-Type application/json;charset=utf-8application/json;charset=utf-8
    授权Authorization <sasToken>
    ServiceBusNotification-FormatServiceBusNotification-Format templatetemplate
    TagsTags "12345""12345"
  4. 配置请求正文,以将“RAW - JSON (application.json)”用于以下 JSON 有效负载: ****   ****  Configure the request BODY to use RAW - JSON (application.json) with the following JSON payload:

    {
    "message" : "Hello from Postman!"
    }
    
  5. 选择“代码”按钮,该按钮位于窗口右上角的“保存”按钮下。 ****   ****  Select the Code button, which is under the Save button on the upper-right of the window. 请求应类似于以下示例:The request should look similar to the following example:

    POST /<hubName>/messages/?api-version=2015-01 HTTP/1.1
    Host: <namespace>.servicebus.chinacloudapi.cn
    Content-Type: application/json;charset=utf-8.
    ServiceBusNotification-Format: template
    Tags: "12345"
    Authorization: <sasToken>
    Cache-Control: no-cache
    Postman-Token: <postmanToken>
    
    {
        "message" : "Hello from Postman!"
    }
    
  6. 选择“发送”按钮。 ****  Select the Send button.

你应该会收到“201 已创建”成功状态代码,并在客户端设备上收到通知。 ****  You should get a 201 Created success status code and receive the notification on the client device.

后续步骤Next steps

现在,你有了一个通过 通知中心 REST API 连接到通知中心的基本 iOS Swift 应用,并且可以发送和接收通知。You now have a basic iOS Swift app connected to a notification hub via the Notification Hubs REST API, and can send and receive notifications. 要详细了解如何向特定设备发送通知,请转到以下教程:To learn more about how to send notifications to specific devices, advance to the following tutorial:

有关详细信息,请参阅以下文章:For more information, see the following articles: