打开 「Push Notifications」Capability
需要有开发者账号才能使用这个Capability

在AppDelegate.swift中申请通知权限,并且得到Apns ID
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UNUserNotificationCenter.current().delegate = self
print(UIDevice.current.identifierForVendor!.uuidString)
// 通知権限取得
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
granted, error in
if granted {
//许可通知以后, 会从苹果的apns服务器,申请apns_id
DispatchQueue.main.async(execute: {
UIApplication.shared.registerForRemoteNotifications()
})
}
}
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
// apns_id申请成功会进入这个方法。
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let uuid = UIDevice.current.identifierForVendor!.uuidString
let deviceName = UIDevice.current.name
// 把取得的Apns_Id 和 uuid mapping在一起插入到数据库中保存。
HttpService.insertApnsTable(uuid: uuid, deviceName: deviceName, apnsId: deviceToken.hexString){
let _: [String:Any] = $0
}
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error.localizedDescription)
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
{
completionHandler([.badge, .sound, .banner])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
completionHandler()
}
}
extension Data {
var hexString: String {
let hexString = map { String(format: "%02.2hhx", $0) }.joined()
return hexString
}
}
消息推送测试可以用Knuff
https://github.com/KnuffApp/Knuff

也可以用nodejs 代码推送通知
Nodejs发送remote通知,主要用apn模块。npm install apn即可使用。
const apn = require("apn");
// 重要,否则会出警告
// MaxListenersExceededWarning: Possible EventEmitter memory leak detected
require('events').EventEmitter.prototype._maxListeners = 100;
var options = {
// 重要推送证书必须的p12文件
pfx: "./xxx/apns.p12",
// p12文件的密码
passphrase: "password",
production: true,
// 重要!!! 不加这句话 有时候会出错。
rejectUnauthorized: false
};
var apnProvider = new apn.Provider(options);
function sendMessage(deviceToken, uuid) {
console.log("sendMessage")
let notification = new apn.Notification();
/// Convenience setter
notification.title = "Hello World";
notification.body = "";
notification.badge = 1;
// 重要必须和app的Bundle identifier能match上。
notification.topic = "com.cn.test";
notification.sound = "default";
// 非常重要, 对应 PushNotificationPayload.apns的category, 只有设定才能显示自定义通知视图。
notification.category = "myCategory";
notification.launchImage = uuid;
// 这里的deviceToken就是苹果的apns_ID
apnProvider.send(notification, deviceToken).then((result) => {
console.log(result.failed)
});
}
module.exports = {
sendMessage
}
另外,AppleWatch可以自定义通知界面,注意点如下
- 注意 PushNotificationPayload.apns文件。category很重要,发送remote通知时,需要指定category的名字,这样才能显示出来自定义的通知。

- NotificationController.swift类,收到通知时,会首先调用NotificationController,在该类didReceive方法中能收到通知的值。

import WatchKit
import SwiftUI
import UserNotifications
import Starscream
// 在AppleWatch中也可以连接WebSocket
class NotificationController: WKUserNotificationHostingController<NotificationView>, WebSocketDelegate {
var uuid:String = ""
override var body: NotificationView {
return NotificationView(uuid: uuid)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
notificationController = self
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
// 消息送来时, 会激活这个方法, 可以从notification中取得消息中的一些值, 并把值传给NotificationView
override func didReceive(_ notification: UNNotification) {
//这里收到通知发过来的uuid
uuid = notification.request.content.launchImageName
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
重要:消息显示5秒后,自动关闭
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
self.performDismissAction()
}
}
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(_):
break
case .disconnected(_,_):
break
case .text(_):
break
case .binary(_):
break
case .ping(_):
break
case .pong(_):
break
case .reconnectSuggested(_):
break
case .error(_):
break
}
}
extension Notification.Name {
static var connected: Notification.Name {
Notification.Name("connected")
}
}
- NotificationView类,这个类就是通知的视图View了,例子中是SwiftUI画的。

import SwiftUI
import Starscream
var socket: WebSocket?
var request: URLRequest?
var notificationController: NotificationController?
struct NotificationView: View {
// 从NotificationController.swift传过来的值
@State var uuid:String
var body: some View {
VStack(spacing: 0) {
VStack{
HStack {
Text(self.uuid)
Spacer()
}
Spacer()
}.frame(width: 160, height: 65)
Spacer().frame(height: 8)
// 通知视图里面也可以连接websocket服务器。
Button(action: {
request = URLRequest(url: URL(string: "ws://localhost:6003")!)
request!.setValue("xxxxxxxx", forHTTPHeaderField: "apikey")
request!.setValue(self.uuid, forHTTPHeaderField: "uuid")
request!.timeoutInterval = 5
socket = WebSocket(request: request!)
socket!.delegate = notificationController
socket!.connect()
})
{
Text("connect WebSocket Server")
}
.frame(width: 155)
}
}
}
- 另外在ios中 websocket主要用Starscream第三方库。下面是Starscream pod文件。
# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'AppleWatchDemo' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for AppleWatchDemo end target 'AppleWatchDemo WatchKit App' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for AppleWatchDemo WatchKit App end target 'AppleWatchDemo WatchKit Extension' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! pod 'Starscream' # Pods for AppleWatchDemo WatchKit Extension end
远程通知也可以自定义通知声音
- 制作完文件后,最简单的方法是将其放入应用程序捆绑包中。就是把自定义的声音文件拖拽到工程里面(Copy items if needed)

- 在Nodejs代码中指定声音文件的名字就可以了。
let notification = new apn.Notification(); notification.title = "XXXXX"; notification.body = message; notification.badge = 0; notification.topic = "XXXXXX"; // ※这里指定声音文件的名字 notification.sound = "ABC.m4a"; // notification.category = "myCategory";
- 注意:音频文件的类型 wav, ma4, caf。自定义声音在播放时必须在30秒以内。如果自定义声音超过该限制,则会播放默认的系统声音。