本文参考文章 iOS 模仿支付宝支付到账推送,播报钱数,看上面写的一些不是很详细遇到了许多问题,这里特意自己总结了一下。将我遇到的问题以及解决方案给罗列出来供大家参考。
iOS10之后的ServiceExtends,如果不是很清楚可以自行百度或者浏览一下iOS10 推送extension之 Service Extension
首先创建一个工程:
打开推送通知注册接受
Background Modes内部的第一个我看有的demo是有够选的,我这这里没有勾选,通知是同样能够收到并且在后台播放的。那么为了不必要的麻烦这里就不勾选了。
然后我们创建通知扩展,通知的扩展能够为我们的app即使在被杀死的情况下也能唤醒30s左右的时间来供我们的app进行语音播报。
这句话的大概意思就是告诉你去激活它,然后在toolbar上面可以选择使用它,我们点击Activate按钮就可以了。
然后你就会发现你的项目工程里面多了一个文件夹,就是你刚才你创建的推送扩展。
然后我们在AppDelegate里面进行通知的注册。
代码如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[self registerRemoteNotification];
return YES;
}
// 注册推送
- (void)registerRemoteNotification{
UIApplication *application = [UIApplication sharedApplication];
application.applicationIconBadgeNumber = 0;
if([application respondsToSelector:@selector(registerUserNotificationSettings:)])
{
UIUserNotificationType notificationTypes = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:notificationTypes categories:nil];
[application registerUserNotificationSettings:settings];
}
#if !TARGET_IPHONE_SIMULATOR
//iOS8 注册APNS
if ([application respondsToSelector:@selector(registerForRemoteNotifications)]) {
[application registerForRemoteNotifications];
}else{
UIRemoteNotificationType notificationTypes = UIRemoteNotificationTypeBadge |
UIRemoteNotificationTypeSound |
UIRemoteNotificationTypeAlert;
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:notificationTypes];
}
#endif
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString *token = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<" withString:@""] stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""];
[UIPasteboard generalPasteboard].string =token;
NSLog(@"device token is %@",token);
}
然后就等之后的推送消息在AppDelegate里面的方法里面接收通知:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo
{
NSLog(@"userInfo ===== %@",userInfo);
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"userInfo === %@",userInfo);
}
另外介绍两个本不属于这个专项的相关问题:Safa Area报错记得勾选去掉
我们在拖入声音文件的时候会有以下提示,这个时候我们的通常做法可以是全部都勾选,(这样做其实是为了方便对iOS10一下声音文件获取),其实对我们iOS10的扩展,我们可以只勾选扩展,因为文件只是在扩展里面才使用。这里作简要的说明一下。
然后就是加入工程的声音文件如下图:
然后我们在NotificationService.m文件内写入我们的声音文件合成代码。并播放声音文件的代码。
代码如下:(亲测通过,没有问题)
#import "NotificationService.h"
#import <AVFoundation/AVFoundation.h>
#define kFileManager [NSFileManager defaultManager]
typedef void(^PlayVoiceBlock)(void);
@interface NotificationService ()<AVAudioPlayerDelegate>
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
//声音文件的播放器
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
//声音文件的路径
@property (nonatomic, strong) NSString *filePath;
// 语音合成完毕之后,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
__weak __typeof(self)weakSelf = self;
/*******************************推荐用法*******************************************/
// 方法3,语音合成,使用AVAudioPlayer播放,成功
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[self hechengVoiceAVAudioPlayerWithFinshBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
// self.contentHandler(self.bestAttemptContent);
}
#pragma mark- 合成音频使用 AVAudioPlayer 播放
- (void)hechengVoiceAVAudioPlayerWithFinshBlock:(PlayVoiceBlock )block
{
if (block) {
self.aVAudioPlayerFinshBlock = block;
}
/************************合成音频并播放*****************************/
AVMutableComposition *composition = [AVMutableComposition composition];
NSArray *fileNameArray = @[@"daozhang",@"1",@"2",@"3",@"4",@"5",@"6",@"1",@"2",@"3",@"4",@"5",@"6",@"1",@"2",@"3",@"4",@"5",@"6"];
CMTime allTime = kCMTimeZero;
for (NSInteger i = 0; i < fileNameArray.count; i++) {
NSString *auidoPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@",fileNameArray[i]] ofType:@"m4a"];
AVURLAsset *audioAsset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:auidoPath]];
// 音频轨道
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:0];
// 音频素材轨道
AVAssetTrack *audioAssetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
// 音频合并 - 插入音轨文件
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset.duration) ofTrack:audioAssetTrack atTime:allTime error:nil];
// 更新当前的位置
allTime = CMTimeAdd(allTime, audioAsset.duration);
}
// 合并后的文件导出 - `presetName`要和之后的`session.outputFileType`相对应。
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:composition presetName:AVAssetExportPresetAppleM4A];
NSString *outPutFilePath = [[self.filePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"xindong.m4a"];
if ([[NSFileManager defaultManager] fileExistsAtPath:outPutFilePath]) {
[[NSFileManager defaultManager] removeItemAtPath:outPutFilePath error:nil];
}
// 查看当前session支持的fileType类型
NSLog(@"---%@",[session supportedFileTypes]);
session.outputURL = [NSURL fileURLWithPath:outPutFilePath];
session.outputFileType = AVFileTypeAppleM4A; //与上述的`present`相对应
session.shouldOptimizeForNetworkUse = YES; //优化网络
[session exportAsynchronouslyWithCompletionHandler:^{
if (session.status == AVAssetExportSessionStatusCompleted) {
NSLog(@"合并成功----%@", outPutFilePath);
NSURL *url = [NSURL fileURLWithPath:outPutFilePath];
self.myPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
self.myPlayer.delegate = self;
[self.myPlayer play];
} else {
// 其他情况, 具体请看这里`AVAssetExportSessionStatus`.
// 播放失败
self.aVAudioPlayerFinshBlock();
}
}];
/************************合成音频并播放*****************************/
}
#pragma mark- AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
if (self.aVAudioPlayerFinshBlock) {
self.aVAudioPlayerFinshBlock();
}
}
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer*)player error:(NSError *)error{
//解码错误执行的动作
}
- (void)audioPlayerBeginInteruption:(AVAudioPlayer*)player{
//处理中断的代码
}
- (void)audioPlayerEndInteruption:(AVAudioPlayer*)player{
//处理中断结束的代码
}
- (NSString *)filePath {
if (!_filePath) {
_filePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
NSString *folderName = [_filePath stringByAppendingPathComponent:@"MergeAudio"];
BOOL isCreateSuccess = [kFileManager createDirectoryAtPath:folderName withIntermediateDirectories:YES attributes:nil error:nil];
if (isCreateSuccess) _filePath = [folderName stringByAppendingPathComponent:@"xindong.m4a"];
}
return _filePath;
}
- (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);
}
当然播放声音也可以使用我们苹果系统的开放库AVSpeechSynthesisVoice
使用AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];创建嗓音
// 创建语音合成器
synthesizer = [[AVSpeechSynthesizer alloc] init];
synthesizer.delegate = self;
// 实例化发声的对象
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:content];
utterance.voice = voice;
utterance.rate = 0.5; // 语速
// 朗读的内容
[synthesizer speakUtterance:utterance];
来实现朗读声音的播放
接下来,我们开始运行我们的代码开始体验我们的结果:
1.我们要选中我们的扩展scheme
然后选中我们的运行的app
接下来就是需要通过推送进行消息的检测的时刻了。
这里推荐一款测试软件SmartPush推送测试工具:(奉上链接地址:SmartPush)
运行然后选择我们的对应的推送证书,设备token
然后我们如果测试在后台推送的数据消息,那么需要多加上字段:"mutable-content":1,
例如:{
"aps":{
"alert":{
"title":"iOS 10 title",
"subtitle":"iOS 10 subtitle",
"body":"iOS 10 body"
},
"mutable-content":1,
"category":"saySomethingCategory",
"sound":"default",
"badge":3
}
}
然后我们点击发送,在前台,后台,杀死状态下就可以测试听语音播报了。
以上就是关于语音播放收款信息的一些搜集和自己的实践整理出来的,
还有后期的打包上架需要选哪个scheme的问题,个人觉得是选择扩展的那个scheme进行打包,还在测试,没有上架呢。待以后有时间及时为大家更新补上。
奉上本文的demo链接地址:艾鑫文学社
2019.4.23号补充:(最近看到还有许多朋友也在不断的点赞,希望再次把我发现的问题告知大家,让关注我的朋友少走弯路。)
测试发现从iOS12.0.1版本之后出现在app后台的模式下不再进行语音播报,经过断点以及log排查,发现是语音合成没有问题,是在此之后的AVAudioPlayer不支持播报导致的问题。暂时尝试过使用iOS的voip推送,是可以让系统重新支持AVAudioPlayer播放处理。但是voip的使用审核可能比较严格,相关使用的同学可以去细致看一下voip的审核问题。(之后的iOS系统什么时候能够后台打开这个AVAudioPlayer播放还需期待。)
下面还有一个微信的收款语音提醒的一个总结微信 iOS 收款到帐语音提醒开发总结,供大家有空参考!