需求布景:完成iOS app没有启动时。在收到转账时完成语音播报收款金额
一、布景
在WWDC2019发布了新的iOS13,苹果不再允许PushKit应用在非voip电话的场景上。这篇文章总结了在iOS13下的语音播报迁移计划以及一些需要注意的问题。
二、技术计划
Notification Service Extension
新的计划是主要是利用了苹果在iOS10中推出的Notification Service Extension(以下简称NSE),当apns的payload上带上”mutable-content”的值为1时,就会进入NSE的代码中。在NSE中,开发者能够更改告诉的内容,利用离线组成或许从后台下载的方法,生成需要播报的内容,经过自定义告诉铃声的方法,达到语音播报提示的意图。NSE计划也是苹果在WWDC2019的Session707上引荐的解决方法。
UNNotificationSound
在NSE中,能够经过给UNNotificationContent中的Sound特点赋值来达到在告诉弹出时播映一段自定义音频的意图。
// The sound file to be played for the notification. The sound must be in the Library/Sounds folder of the app’s data container or the Library/Sounds folder of an app group data container. If the file is not found in a container, the system will look in the app’s bundle.
文档中明确描绘了音频文件的存储路径,以及读取的优先级:
- 主应用中的Library/Sounds文件夹中
- AppGroups共享目录中的Library/Sounds文件夹中
- main bundle中
自定义铃声支持的声音格式包含,aiff、wav以及wav格式,铃声的长度有必要小于30s,不然系统会播映默认的铃声。
AppGroups
由于咱们是在NSE中自定义铃声,所以1和3这两个文件路径咱们是无法拜访的。只能将组成好或许下载到语音音频文件存储到AppGroups下的Library/Sounds文件夹中
,需要在Capablities
中翻开这个AppGroups的能力,即可经过NSFileManager
的containerURLForSecurityApplicationGroupIdentifier:
方法拜访AppGroups的根目录。
四、完成代码如下
NotificationService代码如下
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var isSound:Bool = false
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
let monery = OPBApnsHelper.shared.getMusicArray(with: "168.07")
let name = OPBApnsHelper.shared.mergeVoice(musicArry: monery)
let sound = UNNotificationSound(named: UNNotificationSoundName(name))
bestAttemptContent?.sound = sound
if let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
override func 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.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
把用到的短音频兼并成一个音频文件
/// 兼并音频文件
/// - Parameters:
/// - musicArry: ["1","百","零","6"]
/// - completed: 组成后的文件名称
func mergeVoiceAudiFileName(musicArry:[String],completed:((String?)->Void)?) {
clear(targetPath)
let composition = AVMutableComposition()
var beginTime = CMTime.zero
for audioFileName in musicArry {
if let audioFilePath = Bundle.main.path(forResource: audioFileName, ofType: "mp3") {
guard let audioAsset = AVURLAsset(url: URL(fileURLWithPath: audioFilePath)) as AVURLAsset?,
let audioAssetTrack = audioAsset.tracks(withMediaType: AVMediaType.audio).first else {
continue
}
let audioTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
try audioTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: audioAsset.duration), of: audioAssetTrack, at: beginTime)
beginTime = CMTimeAdd(beginTime, audioAsset.duration)
} catch {
print("Failed to insert audio track: (error)")
return
}
}
}
if !FileManager.default.fileExists(atPath: targetPath) {
do {
try FileManager.default.createDirectory(atPath: targetPath, withIntermediateDirectories: true, attributes: nil)
} catch {
NSLog("创建Sounds文件失利 (targetPath)")
}
}
let fileName = "(now()).m4a"
let fileUrl = URL(string: "file://(targetPath)(fileName)")
guard let url = fileUrl else { return }
let session = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
let outPutFilePath = url
session?.outputURL = outPutFilePath
session?.outputFileType = AVFileType.m4a
session?.shouldOptimizeForNetworkUse = true
session?.exportAsynchronously {
if session?.status == AVAssetExportSession.Status.completed {
print("兼并成功----(outPutFilePath)")
completed?(fileName)
} else {
print("兼并失利")
completed?(nil)
}
}
}
把文字转换为对应的短音频文件名称
func getMusicArray(with numStr: String) -> [String] {
guard let finalStr = makeMusicFrom(numStr) else {
return []
}
// 前部分字段例如:***到账 user_payment是项目自定义的音乐文件
var finalArr = ["user_payment"]
for char in finalStr {
finalArr.append(String(char))
}
return finalArr
}
func makeMusicFrom(_ numstr: String) -> String? {
let numberchar = ["0","1","2","3","4","5","6","7","8","9"]
let inunitchar = ["","十","百","千"]
let unitname = ["","万","亿"]
let valstr = String(format: "%.2f", Double(numstr) ?? 0.00)
var prefix = ""
let head = String(valstr.prefix(valstr.count - 2 - 1))
let foot = String(valstr.suffix(2))
if head == "0" {
prefix = "0"
} else {
var ch = [String]()
for char in head {
ch.append(String(format: "%x", char.asciiValue! - UInt8(ascii: "0")))
}
var zeronum = 0
for i in 0..<ch.count {
let index = (ch.count - 1 - i) % 4
let indexloc = (ch.count - 1 - i) / 4
if ch[i] == "0" {
zeronum += 1
} else {
if zeronum != 0 {
if index != 3 {
prefix += "零"
}
zeronum = 0
}
if ch.count > i {
if let numIndex = Int(ch[i]), numIndex < numberchar.count {
prefix += numberchar[numIndex]
}
}
if inunitchar.count > index {
prefix += inunitchar[index]
}
}
if index == 0 && zeronum < 4 {
if unitname.count > indexloc {
prefix += unitname[indexloc]
}
}
}
}
if prefix.hasPrefix("1十") {
prefix = prefix.replacingOccurrences(of: "1十", with: "十")
}
if foot == "00" {
prefix += "元"
} else {
prefix += String(format: "点%@元", foot)
}
return prefix
}