需求分析
-
推送,大多数时候让人很反感,重要性在降低。
-
一些特殊领域,比如收钱提醒,“支付宝到账20元”这样的消息,还是很喜人的,再加上语音播报,还是容易让人接受。这次,我们要做的就是这种场景。
版本兼容性
-
对于推送来说,
iOS8
和iOS10
是两个重要的分界点,可以划分出3
个阶段,要适配三种,确实够繁琐的。如果有条件,还是让自家的app
从iOS10
开始吧。省事,效果也最好。 -
兼容性都是成本,并且也不符合苹果的发展方向,最低版本能高就尽量高一点。对于推送来说,从
iOS10
开始是非常有必要的,这是一个革新性的变化。
第三方服务
-
可以自己写,代码也不多;
-
极光推送,这个用的人挺多的;以前也一致用。
-
友盟推送。我们用了这个,原因是工程里已经有了友盟统计。
相对来说,极光推送会好用一点;比如,测试消息中,
"mutable-content": "1"
, 这个值的添加,极光推送可以,但是友盟推送做不到。
下面的代码也是友盟封装过的函数。
关于设备号
- 设备号是苹果推送服务器发给我们的,在文件
AppDelegate
中
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
}
- 数据类型是
(NSData *)deviceToken
;这个要注意一下。一般这个推送设备号是要传给后台的,不过后台需要的是字符串,一个64位的字符串。一般采用下面的方式转换。
// NSData => NSString;去掉分隔符与空格
NSString *deviceTokenString = [[[[deviceToken description] stringByReplacingOccurrencesOfString: @"<"withString: @""] stringByReplacingOccurrencesOfString: @">"withString: @""] stringByReplacingOccurrencesOfString: @" "withString: @""];
-
只要调用注册函数,苹果推送服务器就会返回一个
(NSData *)deviceToken
。对于同一台设备,反复注册,苹果返回的deviceToken
都是同一个。 -
如果将app删了重装,或者代码了调用了解除注册接口。那么下次注册,苹果返回的
deviceToken
就不一样了。
推送类型
-
广播:就是所有手机都能收到推送消息
-
组播:这里要用到别名功能。后台推送的时候要带上别名,客户端也要设置别名。那么别名能对上的手机能收到消息,其他就收不到。
/** 绑定一个别名至设备(含账户,和平台类型)
@warning 添加Alias的先决条件是已经成功获取到device_token,否则失败(kUMessageErrorDependsErr)
@param name 账户,例如email
@param type 平台类型,参见本文件头部的`kUMessageAliasType...`,例如:kUMessageAliasTypeSina
@param handle block返回数据,error为获取失败时的信息,responseObject为成功返回的数据
*/
+ (void)addAlias:(NSString * __nonnull)name type:(NSString * __nonnull)type response:(void (^__nonnull)(id __nullable responseObject,NSError * __nullable error))handle;
- 单播:这里要用到
deviceToken
。后台推送的时候要指定deviceToken
,那么只有对应的单个设备才能收到消息。我们这里用的就是这种。
这次,我们用的是单播,收钱了,以用户号和设备号两个唯一标识,给指定用户推送收钱提醒消息。
关于注册:
- 注册的函数
/**
友盟推送的注册接口
@param launchOptions 系统的launchOptions启动消息参数用于处理用户通过消息打开应用相关信息。
@param entity 友盟推送的注册类如果使用默认的注册,Entity设置为nil即可。如需其他的可选择其他参数,具体的参考demo或者文档。
@param completionHandler iOS10授权后的回调。
*/
+ (void)registerForRemoteNotificationsWithLaunchOptions:(NSDictionary * __nullable)launchOptions Entity:(UMessageRegisterEntity * __nullable)entity completionHandler:(void (^ __nullable)(BOOL granted, NSError *_Nullable error))completionHandler;
- 这里有个参数
(NSDictionary * __nullable)launchOptions
,来自启动函数。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return YES;
}
一般情况下
(NSDictionary * __nullable)launchOptions
是空的。如果APP
没开,用户收到推送消息之后,点击,可以打开APP
,这个时候,推送信息就包含在这个(NSDictionary * __nullable)launchOptions
中。
- 也正是因为这个
(NSDictionary * __nullable)launchOptions
,所以大多数的推送处理都是直接放在AppDelegate.m
中。这其实很不好,可以将具体的处理放在其他文件中,做好接口封装和参数传递就可以了。另外,注册函数也不需要程序一启动就执行,理论上可以放在任何位置。只不过不注册,就得不到deviceToken
,收不到消息。
从执行的时间来说,一个相对合理的流程是:在启动的时候马上注册,这样收到苹果返回的
deviceToken
就相对比较早。先存本地,等用户登录成功之后,再将这个deviceToken
上传自己的后台。多次上传也没关系,反正只要不解注册,所生成的deviceToken
是相同的。
关于证书
-
推送需要证书,需要上苹果开发者网站进行申请。
-
在
Xcode
的能力选项中,要打开后台接收消息的能力。 -
证书分为开发证书和发布证书两种。对于推送,推荐直接使用开发证书,调试版本和发布版本可以用这一个证书,比较方便。证书只要上传友盟后台就可以了,按照相关文档操作就可以了。
用
Xcode
直接给手机安装的是Debug
版本,对应的后台是测试模式。如果后台切换到正式模式,需要采用Ad-Hoc
的方式安装Release
版本才能收到消息。
推送开关
-
系统的设置里面有推送开关。这个关了的话,什么推动都收不到。这是系统的,不考虑。
-
这个开关可以做在客户端。方法也很简单,如果关,只要调用解注册函数就可以了。如果开,那么再调用一下注册函数就可以了。不过,这样做有两个问题。一是,每次解注册,重新注册之后,设备的
deviceToken
会发生变化。二是,解注册函数有问题,友盟的注释里写清楚了:
/** 解除RemoteNotification的注册(关闭消息推送,实际调用:[[UIApplication sharedApplication] unregisterForRemoteNotifications])
iOS10.0,iOS10.1两个版本存在系统bug,调用此方法后可能会导致无法再次打开推送
*/
+ (void)unregisterForRemoteNotifications;
- 这个开关可以做在服务端,如果关的话,服务器不推送消息就可以。客户端始终保持可接收推送消息的状态。我们这次就是采用这种方案。
消息接收
-
iOS10
如果指定了代理UNUserNotificationCenterDelegate
,那么,消息的接收将在代理函数中。友盟文档中有示例代码:
// iOS10新增:处理前台收到通知的代理方法
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler API_AVAILABLE(ios(10.0)) {
// 语音播报消息内容
NSDictionary * userInfo = notification.request.content.userInfo;
if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
[UMessage setAutoAlert:NO];
//应用处于前台时的远程推送接受
//必须加这句代码
[UMessage didReceiveRemoteNotification:userInfo];
} else {
//应用处于前台时的本地推送接受
}
completionHandler(UNNotificationPresentationOptionSound|UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert);
}
-
iOS9
或者iOS10
没有指定代理UNUserNotificationCenterDelegate
,那么接收消息在AppDelegate.m
的函数中:
// 推送
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
}
推荐的方式还是最低支持版本改为
iOS10
,上面的AppDelegate.m
中的代理函数就需要出现了。
- 只有
APP
在前台的时候,才能正常处理收到的推送消息。当APP
关闭,或者处于后台或者锁屏的时候,上面接收消息的代码是不能执行的。
当
APP
关闭,或者处于后台或者锁屏的时候,,能够收到推送消息,但是,对应的响应代码是不会执行的。就算开了后台执行能力也没有什么用。
点击响应
-
iOS10
如果指定了代理UNUserNotificationCenterDelegate
,那么,消息的接收将在代理函数中。
// iOS10新增:处理后台点击通知的代理方法
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0)){
NSDictionary * userInfo = response.notification.request.content.userInfo;
if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
// 应用处于后台时的远程推送接受
// 必须加这句代码
[UMessage didReceiveRemoteNotification:userInfo];
} else {
//应用处于后台时的本地推送接受
}
}
- 当
APP
关闭,或者处于后台或者锁屏的时候,只要用户点击了消息,APP
会被唤醒到前台,上面的代码一定会被执行。
语音播报
-
将推送过来的文字消息通过语音的方式播放出来,可以采用第三方的服务。比如,免费的有百度,这是链接
-
系统自带的文字转语音也是有的
#import <AVFoundation/AVFoundation.h>
// 说一句话
- (void)speakSentence:(NSString *)sentence {
// 打开后台播报功能
[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
// 文字转语音
AVSpeechSynthesizer *synthesizer = [[AVSpeechSynthesizer alloc] init];
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:sentence];
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
[synthesizer speakUtterance:utterance];
}
- 只有当
APP
处于前台的时候,才能进行语音播报。当APP
关闭,或者处于后台或者锁屏的时候,语音播报代码不执行,就算开了后台执行语音能力也不起作用。
第三的文字转语音,相对要好听一点;系统自带的声音比较机械,相对来说音质要差一点。
推送扩展
-
当
APP
关闭,或者处于后台或者锁屏的时候,也需要语音播报。那么就要用到UNNotificationServiceExtension
-
这是一个新的
target
,和主工程完全隔离,相当于一个独立的APP
-
相当于安卓的服务,一般是没有界面的。
-
当
APP
关闭,或者处于后台或者锁屏的时候,这个UNNotificationServiceExtension
一直是活跃的。所以,可以把语音播报代码放在这里。 -
友盟的例子是在这里下载图片,实现富文本推送。我们这里是提取推送消息,进行语音播报。
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
// 播报声音
NSString *body = self.bestAttemptContent.userInfo[@"aps"][@"alert"][@"body"];
if (body != nil) {
[self speakSentence:body];
}
// 转发通知
self.contentHandler(self.bestAttemptContent);
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
}
// 说一句话
- (void)speakSentence:(NSString *)sentence {
// 打开后台播报功能
[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
// 文字转语音
AVSpeechSynthesizer *synthesizer = [[AVSpeechSynthesizer alloc] init];
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:sentence];
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
[synthesizer speakUtterance:utterance];
}
- 扩展和主
APP
是两个独立的个体,不能共享数据。如果要进行通讯,那么就要用到App-Group
功能
网友评论