聲明一下,現在國區的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的集成(完結篇)