声明一下,现在国区的App Store应中国特色社会主义的要求,禁止上架有callkit功能的APP,已有的也要整改,删除callkit功能。
因为篇幅太长,所以把这个分成了三部分,希望读者不要打我。。
上一篇文章讲了PushKit的集成和CallKit打电话,那么这篇就来讲讲如何接电话和挂电话还有其他的一些操作。。
需要继续完善上一篇文章的代码哦。。
iOS CallKit与PushKit的集成(一)
接电话
首先来讲一下如果接电话,来到ProviderDelegate中,添加方法:
func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((Error?) -> Void)?) {
//准备向系统报告一个 call update 事件,它包含了所有的来电相关的元数据。
let update = self.callUpdate(handle: handle, hasVideo: hasVideo)
//调用 CXProvider 的reportIcomingCall(with:update:completion:)方法通知系统有来电。
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error == nil {
//completion 回调会在系统处理来电时调用。如果没有任何错误,你就创建一个 Call 实例,将它添加到 CallManager 的通话列表。
let call = Call(uuid: uuid, handle: handle)
self.callManager.add(call: call)
}
//调用 completion,如果它不为空的话。
completion?(error)
}
}
这个方法需要在所有接电话的地方手动调用,需要根据自己的业务逻辑来判断。还有就是不要忘了iOS的版本兼容哦。。
在ProviderDelegate中实现系统接电话的代理:
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
//从 callManager 中获得一个引用,UUID 指定为要接听的动画的 UUID。
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
//设置通话要用的 audio session 是 App 的责任。系统会以一个较高的优先级来激活这个 session。
configureAudioSession()
//通过调用 answer,你会表明这个通话现在激活
call.answer()
//在这里添加自己App接电话的逻辑
//在处理一个 CXAction 时,重要的一点是,要么你拒绝它(fail),要么满足它(fullfill)。如果处理过程中没有发生错误,你可以调用 fullfill() 表示成功。
action.fulfill()
}
回到AppDelegate中,找到之前写的PushKit收到推送的代理方法,在里面添加:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
guard type == .voIP else {
log.info("Callkit& pushRegistry didReceiveIncomingPush But Not VoIP")
return
}
log.info("Callkit& pushRegistry didReceiveIncomingPush")
//别忘了在这里加上你们自己接电话的逻辑,比如连接聊天服务器啥的,不然这个电话打不通的
if let uuidString = payload.dictionaryPayload["UUID"] as? String,
let handle = payload.dictionaryPayload["handle"] as? String,
let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool,
let uuid = UUID(uuidString: uuidString)
{
if #available(iOS 10.0, *) {
ProviderDelegate.shared.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: { (error) in
if let e = error {
log.info("CallKit& displayIncomingCall Error \(e)")
}
})
} else {
// Fallback on earlier versions
}
}
}
至此,CallKit接电话的逻辑完成了,你只需要在合适的地方调用reportIncomingCall就可以调出系统的通话页面了。
挂电话
来到CallKitManager中,添加方法:
func end(call: Call) {
//先创建一个 CXEndCallAction。将通话的 UUID 传递给构造函数,以便在后面可以识别通话。
let endCallAction = CXEndCallAction(call: call.uuid)
//然后将 action 封装成 CXTransaction,以便发送给系统。
let transaction = CXTransaction(action: endCallAction)
requestTransaction(transaction)
}
来到ProviderDelegate中,实现系统代理:
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
//从 callManager 获得一个 call 对象。
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
//当 call 即将结束时,停止这次通话的音频处理。
stopAudio()
//调用 end() 方法修改本次通话的状态,以允许其他类和新的状态交互。
call.end()
//在这里添加自己App挂断电话的逻辑
//将 action 标记为 fulfill。
action.fulfill()
//当你不再需要这个通话时,可以让 callManager 回收它。
callManager.remove(call: call)
}
添加完之后,只需要在你自己App挂断电话的地方调用:
if #available(iOS 10.0, *) {
if let call = CallKitManager.shared.calls.first { //因为我们这里不支持群通话,所以一次只有一个call
CallKitManager.shared.end(call: call)
}
}
就可以了。。这里的CallKitManager.shared.calls保存了所有CallKit的通话。是咱们自己写的工具类哦,忘了的话自己翻翻上篇文章。
至此,CallKit挂电话的逻辑结束。。
通话暂时挂起
来到CallKitManager中,添加方法:
func setHeld(call: Call, onHold: Bool) {
//这个 CXSetHeldCallAction 包含了通话的 UUID 以及保持状态
let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
let transaction = CXTransaction()
transaction.addAction(setHeldCallAction)
requestTransaction(transaction)
}
来到ProviderDelegate中,实现系统代理:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
//获得 CXCall 对象之后,我们要根据 action 的 isOnHold 属性来设置它的 state。
call.state = action.isOnHold ? .held:.active
//根据状态的不同,分别进行启动或停止音频会话。
if call.state == .held {
stopAudio()
} else {
startAudio()
}
//在这里添加你们自己的通话挂起逻辑
action.fulfill()
}
添加完之后,只需要在你自己App通话暂时挂起的地方调用:
if #available(iOS 10.0, *) {
if let call = CallKitManager.shared.calls.first {
CallKitManager.shared.setHeld(call: call, onHold: true)
}
}
就可以了。。
至此,CallKit通话暂时挂起的逻辑结束。。
麦克风静音
来到CallKitManager中,添加方法:
func setMute(call: Call, muted: Bool) {
//CXSetMutedCallAction设置麦克风静音
let setMuteCallAction = CXSetMutedCallAction(call: call.uuid, muted: muted)
let transaction = CXTransaction()
transaction.addAction(setMuteCallAction)
requestTransaction(transaction)
}
来到ProviderDelegate中,实现系统代理:
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
//获得 CXCall 对象之后,我们要根据 action 的 ismuted 属性来设置它的 state。
call.state = action.isMuted ? .muted : .active
//在这里添加你们自己的麦克风静音逻辑
action.fulfill()
}
添加完之后,只需要在你自己App麦克风静音的地方调用:
if #available(iOS 10.0, *) {
if let call = CallKitManager.shared.calls.first {
CallKitManager.shared.setMute(call: call, muted: true)
}
}
就可以了。。
至此,CallKit麦克风静音的逻辑结束。。
到这里,在App内互动的CallKit的基本功能都已经集成完毕,其实到后面大家就能看出来,文章中所有的功能实现,都是先在CallKitManager写用户需要调用的方法,在ProviderDelegate里面实现系统的代理方法,并且加上自己的通话逻辑。
关于系统扬声器与听筒的切换
这里不讲如何切换扬声器与听筒,只讲如何监听切换,保持App内通话页面免提的状态跟系统通话页面的一致。
在自己的通话页面上添加通知监听:
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVAudioSessionRouteChange, object: nil, queue: OperationQueue.main) {[weak self] (noti) in
guard let w = self else { return }
if #available(iOS 10.0, *) {
let route = AVAudioSession.sharedInstance().currentRoute
for desc in route.outputs {
if desc.portType == "Speaker" {
// "免提功能已开启"
} else {
// "对方已接通,请使用听筒接听"
}
}
}
}
至此,CallKit的主要功能集成完毕,下一篇文章将继续讲解如何从系统通话记录中直接拨打App的电话。
iOS CallKit与PushKit的集成(完结篇)