iOS CallKit與PushKit的整合(一)
宣告一下,現在國區的App Store應中國特色社會主義的要求,禁止上架有callkit功能的APP,已有的也要整改,刪除callkit功能。
很多VoIP的開發者發現,升級到Xcode9以後,原來的Voice over IP的選項消失了,需要自行去info.plist中新增App provides Voice over IP services。
隱藏了這個選項其實是為了強制大家使用CallKit+PushKit來做VoIP的應用程式。
我們的經驗是基於PushKit的VoIP應用程式比那些使用傳統VoIP架構的應用程式更可靠,更省電。
具體來說,我們鼓勵VoIP應用程式充分利用iOS 10 SDK中的新框架CallKit,從根本上改善了VoIP應用程式的使用者體驗。
另外,請注意,macOS 10.12 Sierra不支援Xcode 7。
在某些時候,對傳統VoIP架構的支援將被刪除,於是所有的VoIP應用將不得不轉移到新的基於PushKit的VoIP架構。
這裡我就來簡單介紹一下如何整合CallKit與PushKit
要整合,首先就要匯入framework,圖中的三個framework都要匯入,第一個framework是從通訊錄中直接撥打App電話所需要的。
PushKit
這個是iOS8後才支援的框架,如果你的專案現在還在支援iOS7,那麼你可以以辭職為籌碼去跟產品經理鬥智鬥勇了。
整合PushKit很簡單,跟註冊普通的APNS推送一個樣,先去註冊:
//import PushKit 這個加在檔案頭部。大家都是老司機了,缺標頭檔案自己加。
let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]
然後註冊成功沒呢?看這個代理方法:
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
if pushCredentials.token.count > 0 {
var token = NSString(format: "%@", pushCredentials.token as CVarArg) as String
print("pushRegistry credentialsToken \(token)")
}
}
大家注意了,這裡的token跟APNS的deviceToken雖然長度和格式一樣,但是內容是不同的。這是因為蘋果需要區分這是PushKit的推送還是APNS的推送。
註冊好token後,就可以上傳給自己的伺服器了。然後需要自己的伺服器發推送。
這裡就牽扯到證書的問題了,首先要知道的是,VoIP的PushKit推送證書跟APNS的是兩個不同的證書,需要自己去生成,然後匯出p12檔案給伺服器。
匯出證書這裡就不做過多贅述,只要知道一點,VoIP的PushKit證書只有Product環境的,但是測試環境也能使。
匯出p12檔案,注意匯出的檔案大小應該有6kb,如果只有一半說明你沒把公鑰導進去。
下面就可以測試推送啦。。。
我們先來看看在哪裡接推送,Appdelegate裡面有這個方法:
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("pushRegistry didReceiveIncomingPush")
}
這個方法裡的PKPushPayload裡有個dictionaryPayload,是個字典,作用跟APNS裡的info一個樣。。。要學會舉一反三吶。。
至此,一套PushKit的推送流程就搭建好了。。如果伺服器沒搞好,但是想測試的話,可以用這個:
https://github.com/noodlewerk/NWPusher
一個很牛逼的Push測試軟體。用的HTTP2,只要證書選對,token填對,就能發啦。。
CallKit
重點來了。。
對於CallKit首先要明確一點。在你使用的時候,不要把他看成一個很複雜的框架,他就是系統的打電話頁面,跟你自己寫的打電話頁面一樣一樣的;只要是頁面,就可以呼叫顯示和消失,可以對上面的按鈕進行操作。
工欲善其事必先利其器,我們首先來建立幾個工具類:
第一個,Call類,用來管理CallKit的電話,注意是管理CallKit的電話,跟你自己的電話邏輯不衝突!!
enum CallState { //狀態都能看得懂吧。。看不懂的自己打個電話想想流程。
case connecting
case active
case held
case ended
case muted
}
enum ConnectedState {
case pending
case complete
}
class Call {
let uuid: UUID //來電的唯一識別符號
let outgoing: Bool //是撥打的還是接聽的
let handle: String //後面很多地方用得到,名字都是handle哈,可以理解為電話號碼,其實就是自己App裡被呼叫方的賬號(至少我們是這樣的)。。
var state: CallState = .ended {
didSet {
stateChanged?()
}
}
var connectedState: ConnectedState = .pending {
didSet {
connectedStateChanged?()
}
}
var stateChanged: (() -> Void)?
var connectedStateChanged: (() -> Void)?
init(uuid: UUID, outgoing: Bool = false, handle: String) {
self.uuid = uuid
self.outgoing = outgoing
self.handle = handle
}
func start(completion: ((_ success: Bool) -> Void)?) {
completion?(true)
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 3) {
self.state = .connecting
self.connectedState = .pending
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
self.state = .active
self.connectedState = .complete
}
}
}
func answer() {
state = .active
}
func end() {
state = .ended
}
}
然後建立一個Audio類,用來管理音訊,鈴聲的播放。
func configureAudioSession() { //這裡必須這麼做。。不然會出現沒鈴聲的情況。原因嘛。。我也不知道。。
log.info("Callkit& Configuring audio session")
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
try session.setMode(AVAudioSessionModeVoiceChat)
} catch (let error) {
log.info("Callkit& Error while configuring audio session: \(error)")
}
}
func startAudio() {
log.info("Callkit& Starting audio")
//開始播放鈴聲
}
func stopAudio() {
log.info("Callkit& Stopping audio")
//停止播放鈴聲
}
工具類都做好了,下面開始整合CallKit~~~~~~~~~~~
首先,建立一個CallKitManager的類,只要是使用者發起的動作,都跟這個類有關係。
@available(iOS 10.0, *)
class CallKitManager {
static let shared = CallKitManager()
var callsChangedHandler: (() -> Void)?
private let callController = CXCallController()
private(set) var calls = [Call]()
private init(){}
func callWithUUID(uuid: UUID) -> Call? {
guard let index = calls.index(where: { $0.uuid == uuid }) else {
return nil
}
return calls[index]
}
func add(call: Call) {
calls.append(call)
call.stateChanged = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.callsChangedHandler?()
}
callsChangedHandler?()
}
func remove(call: Call) {
guard let index = calls.index(where: { $0 === call }) else { return }
calls.remove(at: index)
callsChangedHandler?()
}
func removeAllCalls() {
calls.removeAll()
callsChangedHandler?()
}
}
想必大家都發現了,現在CallKitManager裡面只有callController跟CallKit有關係,不急,我們一點一點的把這個類豐富起來。這麼做是為了加深理解,並不是簡單的複製程式碼,到時候出了問題知道在哪進行改動。
現在CallKitManager裡面的函式,其實是用了我們自己寫的Call類,對CallKit做一個邏輯的管理,大家發現了,這裡就跟佇列一個樣,add、remove、removeAll、callWithUUID(根據uuid去找到這個call物件)。
然後我們來看一下callController這個CXCallController物件,CallKitManager裡面目前唯一與CallKit有關係就是他。CXCallController可以讓系統收到App的一些Request,使用者的action,App內部的事件。
我們現在來豐富CallKitManager,先從打電話開始:
新增下列程式碼:
func startCall(handle: String, videoEnabled: Bool) {
//一個 CXHandle 物件表示了一次操作,同時指定了操作的型別和值。App支援對電話號碼進行操作,因此我們在操作中指定了電話號碼。
let handle = CXHandle(type: .phoneNumber, value: handle)
//一個 CXStartCallAction 用一個 UUID 和一個操作作為輸入。
let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
//你可以通過 action 的 isVideo 屬性指定通話是音訊還是視訊。
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction(action: startCallAction)
requestTransaction(transaction)
}
//呼叫 callController 的 request(_:completion:) 。系統會請求 CXProvider 執行這個 CXTransaction,這會導致你實現的委託方法被呼叫。
private func requestTransaction(_ transaction: CXTransaction) {
callController.request(transaction) { error in
if let error = error {
log.info("Callkit& Error requesting transaction: \(error)")
} else {
log.info("Callkit& Requested transaction successfully")
}
}
}
是不是迫不及待的想呼叫一下這個函式了?但是呼叫後發現,並沒有什麼事情發生。。
其實就是這樣。。因為你只向系統傳送了要打電話的請求,但是系統也要告訴你你現在可不可以打,這樣才叫與系統通訊嘛。。不能只是單方面的要求,還需要對方的應答。這裡其實就跟伺服器請求一個樣,發要求,等回應,收到回應後進行下一步操作。
那麼這裡,我們就需要來接收系統的回應了。。怎麼接收到呢?
我們新建一個類,名字叫ProviderDelegate,繼承自誰不重要,重要的是需要遵循CXProviderDelegate這個代理。
@available(iOS 10.0, *)
class ProviderDelegate: NSObject, CXProviderDelegate {
static let shared = ProviderDelegate()
//ProviderDelegate 需要和 CXProvider 和 CXCallController 打交道,因此保持兩個對二者的引用。
private let callManager: CallKitManager //還記得他裡面有個callController嘛。。
private let provider: CXProvider
override init() {
self.callManager = CallKitManager.shared
//用一個 CXProviderConfiguration 初始化 CXProvider,前者在後面會定義成一個靜態屬性。CXProviderConfiguration 用於定義通話的行為和能力。
provider = CXProvider(configuration: type(of: self).providerConfiguration)
super.init()
//為了能夠響應來自於 CXProvider 的事件,你需要設定它的委託。
provider.setDelegate(self, queue: nil)
}
//通過設定CXProviderConfiguration來支援視訊通話、電話號碼處理,並將通話群組的數字限制為 1 個,其實光看屬性名大家也能看得懂吧。
static var providerConfiguration: CXProviderConfiguration {
let providerConfiguration = CXProviderConfiguration(localizedName: "Mata Chat")//這裡填你App的名字哦。。
providerConfiguration.supportsVideo = false
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.maximumCallGroups = 1
providerConfiguration.supportedHandleTypes = [.phoneNumber]
return providerConfiguration
}
//這個方法牛逼了,它是用來更新系統電話屬性的。。
func callUpdate(handle: String, hasVideo: Bool) -> CXCallUpdate {
let update = CXCallUpdate()
update.localizedCallerName = "ParadiseDuo"//這裡是系統通話記錄裡顯示的聯絡人名稱哦。需要顯示什麼按照你們的業務邏輯來。
update.supportsGrouping = false
update.supportsHolding = false
update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) //填了聯絡人的名字,怎麼能不填他的handle('電話號碼')呢,具體填什麼,根據你們的業務邏輯來
update.hasVideo = hasVideo
return update
}
//CXProviderDelegate 唯一一個必須實現的代理方法!!當 CXProvider 被 reset 時,這個方法被呼叫,這樣你的 App 就可以清空所有去電,會到乾淨的狀態。在這個方法中,你會停止所有的撥出音訊會話,然後拋棄所有啟用的通話。
func providerDidReset(_ provider: CXProvider) {
stopAudio()
for call in callManager.calls {
call.end()
}
callManager.removeAllCalls()
//這裡新增你們結束通話電話或拋棄所有啟用的通話的程式碼。。
}
}
上面的ProviderDelegate準備工作做好後,繼續我們打電話的邏輯,在ProviderDelegate新增代理方法:
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
//向系統通訊錄更新通話記錄
let update = self.callUpdate(handle: action.handle.value, hasVideo: action.isVideo)
provider.reportCall(with: action.callUUID, updated: update)
let call = Call(uuid: action.callUUID, outgoing: true, handle: action.handle.value)
//當我們用 UUID 建立出 Call 物件之後,我們就應該去配置 App 的音訊會話。和呼入通話一樣,你的唯一任務就是配置。真正的處理在後面進行,也就是在 provider(_:didActivate) 委託方法被呼叫時
configureAudioSession()
//delegate 會監聽通話的生命週期。它首先會會報告的就是撥出通話開始連線。當通話最終連上時,delegate 也會被通知。
call.connectedStateChanged = { [weak self] in
guard let w = self else {
return
}
if call.connectedState == .pending {
w.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
} else if call.connectedState == .complete {
w.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
}
}
//呼叫 call.start() 方法會導致 call 的生命週期變化。如果連線成功,則標記 action 為 fullfill。
call.start { [weak self] (success) in
guard let w = self else {
return
}
if success {
//這裡填寫你們App內打電話的邏輯。。
w.callManager.add(call: call)
//所有的Action只有呼叫了fulfill()之後才算執行完畢。
action.fulfill()
} else {
action.fail()
}
}
}
//當系統啟用 CXProvider 的 audio session時,委託會被呼叫。這給你一個機會開始處理通話的音訊。
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
startAudio() //一定要記得播放鈴聲吶。。
}
至此,通過CallKit撥打電話的邏輯就完成了。你只要在自己App需要打電話的地方,呼叫
CallKitManager.shared.startCall(handle: userName, videoEnabled: false)就行啦。。但是有一點需要注意,CallKit只有iOS 10以上支援,所以iOS 10以下的手機還是要支援你們原來打電話的邏輯,像這樣:
if #available(iOS 10.0, *) {
CallKitManager.shared.startCall(handle:userName, videoEnabled: false)
} else {
//原來打電話的邏輯
}
然後當你興沖沖的去用CallKit打電話的時候,卻發現彈出的是自己的通話頁面。。。T_T
但是此時你檢視系統的通話記錄,應該會發現通話記錄裡面新增了一條從自己App打出去的記錄。這樣就說明CallKit撥打電話接入成功了!
因為內容較多,分成了三篇文章,下一篇講如何接電話,繼續完善這篇文章的程式碼。
iOS CallKit與PushKit的整合(二)
相關文章
- iOS VoIP電話:CallKit與PushKit的應用iOS
- iOS專案的持續整合與管理iOS
- iOS 極光推送整合與開發iOS
- iOS持續整合(一)——fastlane 使用iOSAST
- Weex 學習與實踐(二):iOS 整合的 tipsiOS
- iOS整合 Flutter 混合工程開發一iOSFlutter
- 微信支付iOS整合與二次封裝iOS封裝
- iOS開發之整合高德地圖(一)iOS地圖
- 整合支付寶錢包支付 iOS SDK 的方法與經驗iOS
- iOS 持續整合iOS
- Practice - iOS 專案持續整合實踐(一)iOS
- Practice – iOS 專案持續整合實踐(一)iOS
- Flutter系列一:探究Flutter App在iOS宿主App中的整合FlutterAPPiOS
- iOS原生專案整合 React Native 一 導航iOSReact Native
- PayPal-iOS-整合攻略iOS
- fastlane 的整合與使用AST
- Cassandra與Kafka的整合Kafka
- iOS 整合Ping++支付,繞過一些坑iOS
- 乾貨系列:ios支付寶的整合iOS
- WAS與IHS整合的安裝與配置
- iOS環信整合(附demo)iOS
- iOS整合個推小結iOS
- iOS整合Fabric & Crashlytics (2)iOS
- cacti+nagios 之cacti整合nagios(四)iOS
- CAS與Spring的整合Spring
- struts與spring 的整合Spring
- Android與iOS/WP8跨平臺整合設計與開發_專欄AndroidiOS
- iOS - 新增一個全域性懸浮按鈕(整合pods版)iOS
- 環信3.0iOS客戶端的整合iOS客戶端
- iOS自動整合打包釋出iOS
- iOS 持續整合系列 – 開篇iOS
- ios 百度鷹眼整合iOS
- IOS中 Block簡介與用法(一)iOSBloC
- 應用出海,如何使用蘋果 CallKit 提升網路通話體驗蘋果
- python django與celery的整合PythonDjango
- KubeSphere 與 Jenkins 的整合解析Jenkins
- JBOSS與JBuilder的整合問題!UI
- CRM與ERP的整合與關係(轉)