聲明一下,現(xiàn)在國區(qū)的App Store應(yīng)中國特色社會(huì)主義的要求吓肋,禁止上架有callkit功能的APP,已有的也要整改瑰艘,刪除callkit功能是鬼。
很多VoIP的開發(fā)者發(fā)現(xiàn),升級到Xcode9以后紫新,原來的Voice over IP的選項(xiàng)消失了均蜜,需要自行去info.plist中添加App provides Voice over IP services。
隱藏了這個(gè)選項(xiàng)其實(shí)是為了強(qiáng)制大家使用CallKit+PushKit來做VoIP的應(yīng)用程序芒率。
我們的經(jīng)驗(yàn)是基于PushKit的VoIP應(yīng)用程序比那些使用傳統(tǒng)VoIP架構(gòu)的應(yīng)用程序更可靠囤耳,更省電。
具體來說偶芍,我們鼓勵(lì)VoIP應(yīng)用程序充分利用iOS 10 SDK中的新框架CallKit充择,從根本上改善了VoIP應(yīng)用程序的用戶體驗(yàn)。
另外匪蟀,請注意椎麦,macOS 10.12 Sierra不支持Xcode 7。
在某些時(shí)候材彪,對傳統(tǒng)VoIP架構(gòu)的支持將被刪除观挎,于是所有的VoIP應(yīng)用將不得不轉(zhuǎn)移到新的基于PushKit的VoIP架構(gòu)琴儿。
這里我就來簡單介紹一下如何集成CallKit與PushKit
要集成,首先就要導(dǎo)入framework嘁捷,圖中的三個(gè)framework都要導(dǎo)入造成,第一個(gè)framework是從通訊錄中直接撥打App電話所需要的。
PushKit
這個(gè)是iOS8后才支持的框架雄嚣,如果你的項(xiàng)目現(xiàn)在還在支持iOS7晒屎,那么你可以以辭職為籌碼去跟產(chǎn)品經(jīng)理斗智斗勇了。
集成PushKit很簡單缓升,跟注冊普通的APNS推送一個(gè)樣鼓鲁,先去注冊:
//import PushKit 這個(gè)加在文件頭部。大家都是老司機(jī)了仔沿,缺頭文件自己加。
let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]
然后注冊成功沒呢尺棋?看這個(gè)代理方法:
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雖然長度和格式一樣,但是內(nèi)容是不同的膘螟。這是因?yàn)樘O果需要區(qū)分這是PushKit的推送還是APNS的推送成福。
注冊好token后,就可以上傳給自己的服務(wù)器了荆残。然后需要自己的服務(wù)器發(fā)推送奴艾。
這里就牽扯到證書的問題了,首先要知道的是内斯,VoIP的PushKit推送證書跟APNS的是兩個(gè)不同的證書蕴潦,需要自己去生成,然后導(dǎo)出p12文件給服務(wù)器俘闯。
導(dǎo)出證書這里就不做過多贅述潭苞,只要知道一點(diǎn),VoIP的PushKit證書只有Product環(huán)境的真朗,但是測試環(huán)境也能使此疹。
導(dǎo)出p12文件,注意導(dǎo)出的文件大小應(yīng)該有6kb遮婶,如果只有一半說明你沒把公鑰導(dǎo)進(jìn)去蝗碎。
下面就可以測試推送啦。旗扑。蹦骑。
我們先來看看在哪里接推送,Appdelegate里面有這個(gè)方法:
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")
}
這個(gè)方法里的PKPushPayload里有個(gè)dictionaryPayload臀防,是個(gè)字典脊串,作用跟APNS里的info一個(gè)樣辫呻。。琼锋。要學(xué)會(huì)舉一反三吶放闺。。
至此缕坎,一套PushKit的推送流程就搭建好了怖侦。。如果服務(wù)器沒搞好谜叹,但是想測試的話匾寝,可以用這個(gè):
https://github.com/noodlewerk/NWPusher
一個(gè)很牛逼的Push測試軟件。用的HTTP2荷腊,只要證書選對艳悔,token填對,就能發(fā)啦女仰。猜年。
CallKit
重點(diǎn)來了。疾忍。
對于CallKit首先要明確一點(diǎn)乔外。在你使用的時(shí)候,不要把他看成一個(gè)很復(fù)雜的框架一罩,他就是系統(tǒng)的打電話頁面杨幼,跟你自己寫的打電話頁面一樣一樣的;只要是頁面聂渊,就可以調(diào)用顯示和消失差购,可以對上面的按鈕進(jìn)行操作。
工欲善其事必先利其器汉嗽,我們首先來創(chuàng)建幾個(gè)工具類:
第一個(gè)歹撒,Call類,用來管理CallKit的電話诊胞,注意是管理CallKit的電話暖夭,跟你自己的電話邏輯不沖突!撵孤!
enum CallState { //狀態(tài)都能看得懂吧迈着。⌒奥耄看不懂的自己打個(gè)電話想想流程裕菠。
case connecting
case active
case held
case ended
case muted
}
enum ConnectedState {
case pending
case complete
}
class Call {
let uuid: UUID //來電的唯一標(biāo)識符
let outgoing: Bool //是撥打的還是接聽的
let handle: String //后面很多地方用得到,名字都是handle哈闭专,可以理解為電話號碼奴潘,其實(shí)就是自己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
}
}
然后建立一個(gè)Audio類画髓,用來管理音頻掘剪,鈴聲的播放。
func configureAudioSession() { //這里必須這么做奈虾。夺谁。不然會(huì)出現(xiàn)沒鈴聲的情況。原因嘛肉微。匾鸥。我也不知道。碉纳。
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~~~~~~~~~~~
首先,建立一個(gè)CallKitManager的類劳曹,只要是用戶發(fā)起的動(dòng)作奴愉,都跟這個(gè)類有關(guān)系。
@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?()
}
}
想必大家都發(fā)現(xiàn)了厚者,現(xiàn)在CallKitManager里面只有callController跟CallKit有關(guān)系躁劣,不急迫吐,我們一點(diǎn)一點(diǎn)的把這個(gè)類豐富起來库菲。這么做是為了加深理解,并不是簡單的復(fù)制代碼志膀,到時(shí)候出了問題知道在哪進(jìn)行改動(dòng)熙宇。
現(xiàn)在CallKitManager里面的函數(shù),其實(shí)是用了我們自己寫的Call類溉浙,對CallKit做一個(gè)邏輯的管理烫止,大家發(fā)現(xiàn)了,這里就跟隊(duì)列一個(gè)樣戳稽,add馆蠕、remove、removeAll惊奇、callWithUUID(根據(jù)uuid去找到這個(gè)call對象)互躬。
然后我們來看一下callController這個(gè)CXCallController對象,CallKitManager里面目前唯一與CallKit有關(guān)系就是他颂郎。CXCallController可以讓系統(tǒng)收到App的一些Request吼渡,用戶的action,App內(nèi)部的事件乓序。
我們現(xiàn)在來豐富CallKitManager寺酪,先從打電話開始:
添加下列代碼:
func startCall(handle: String, videoEnabled: Bool) {
//一個(gè) CXHandle 對象表示了一次操作坎背,同時(shí)指定了操作的類型和值。App支持對電話號碼進(jìn)行操作寄雀,因此我們在操作中指定了電話號碼得滤。
let handle = CXHandle(type: .phoneNumber, value: handle)
//一個(gè) CXStartCallAction 用一個(gè) UUID 和一個(gè)操作作為輸入。
let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
//你可以通過 action 的 isVideo 屬性指定通話是音頻還是視頻咙俩。
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction(action: startCallAction)
requestTransaction(transaction)
}
//調(diào)用 callController 的 request(_:completion:) 耿戚。系統(tǒng)會(huì)請求 CXProvider 執(zhí)行這個(gè) CXTransaction,這會(huì)導(dǎo)致你實(shí)現(xiàn)的委托方法被調(diào)用阿趁。
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")
}
}
}
是不是迫不及待的想調(diào)用一下這個(gè)函數(shù)了膜蛔?但是調(diào)用后發(fā)現(xiàn),并沒有什么事情發(fā)生脖阵。皂股。
其實(shí)就是這樣。命黔。因?yàn)槟阒幌蛳到y(tǒng)發(fā)送了要打電話的請求呜呐,但是系統(tǒng)也要告訴你你現(xiàn)在可不可以打,這樣才叫與系統(tǒng)通訊嘛悍募。蘑辑。不能只是單方面的要求,還需要對方的應(yīng)答坠宴。這里其實(shí)就跟服務(wù)器請求一個(gè)樣洋魂,發(fā)要求,等回應(yīng)喜鼓,收到回應(yīng)后進(jìn)行下一步操作副砍。
那么這里,我們就需要來接收系統(tǒng)的回應(yīng)了庄岖。豁翎。怎么接收到呢?
我們新建一個(gè)類隅忿,名字叫ProviderDelegate心剥,繼承自誰不重要,重要的是需要遵循CXProviderDelegate這個(gè)代理背桐。
@available(iOS 10.0, *)
class ProviderDelegate: NSObject, CXProviderDelegate {
static let shared = ProviderDelegate()
//ProviderDelegate 需要和 CXProvider 和 CXCallController 打交道优烧,因此保持兩個(gè)對二者的引用。
private let callManager: CallKitManager //還記得他里面有個(gè)callController嘛牢撼。匙隔。
private let provider: CXProvider
override init() {
self.callManager = CallKitManager.shared
//用一個(gè) CXProviderConfiguration 初始化 CXProvider,前者在后面會(huì)定義成一個(gè)靜態(tài)屬性。CXProviderConfiguration 用于定義通話的行為和能力纷责。
provider = CXProvider(configuration: type(of: self).providerConfiguration)
super.init()
//為了能夠響應(yīng)來自于 CXProvider 的事件捍掺,你需要設(shè)置它的委托。
provider.setDelegate(self, queue: nil)
}
//通過設(shè)置CXProviderConfiguration來支持視頻通話再膳、電話號碼處理挺勿,并將通話群組的數(shù)字限制為 1 個(gè),其實(shí)光看屬性名大家也能看得懂吧喂柒。
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
}
//這個(gè)方法牛逼了灾杰,它是用來更新系統(tǒng)電話屬性的蚊丐。。
func callUpdate(handle: String, hasVideo: Bool) -> CXCallUpdate {
let update = CXCallUpdate()
update.localizedCallerName = "ParadiseDuo"http://這里是系統(tǒng)通話記錄里顯示的聯(lián)系人名稱哦艳吠。需要顯示什么按照你們的業(yè)務(wù)邏輯來麦备。
update.supportsGrouping = false
update.supportsHolding = false
update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) //填了聯(lián)系人的名字,怎么能不填他的handle('電話號碼')呢昭娩,具體填什么凛篙,根據(jù)你們的業(yè)務(wù)邏輯來
update.hasVideo = hasVideo
return update
}
//CXProviderDelegate 唯一一個(gè)必須實(shí)現(xiàn)的代理方法!栏渺!當(dāng) CXProvider 被 reset 時(shí)呛梆,這個(gè)方法被調(diào)用,這樣你的 App 就可以清空所有去電磕诊,會(huì)到干凈的狀態(tài)填物。在這個(gè)方法中,你會(huì)停止所有的呼出音頻會(huì)話秀仲,然后拋棄所有激活的通話融痛。
func providerDidReset(_ provider: CXProvider) {
stopAudio()
for call in callManager.calls {
call.end()
}
callManager.removeAllCalls()
//這里添加你們掛斷電話或拋棄所有激活的通話的代碼壶笼。神僵。
}
}
上面的ProviderDelegate準(zhǔn)備工作做好后,繼續(xù)我們打電話的邏輯覆劈,在ProviderDelegate添加代理方法:
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
//向系統(tǒng)通訊錄更新通話記錄
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)
//當(dāng)我們用 UUID 創(chuàng)建出 Call 對象之后保礼,我們就應(yīng)該去配置 App 的音頻會(huì)話。和呼入通話一樣责语,你的唯一任務(wù)就是配置炮障。真正的處理在后面進(jìn)行,也就是在 provider(_:didActivate) 委托方法被調(diào)用時(shí)
configureAudioSession()
//delegate 會(huì)監(jiān)聽通話的生命周期坤候。它首先會(huì)會(huì)報(bào)告的就是呼出通話開始連接胁赢。當(dāng)通話最終連上時(shí),delegate 也會(huì)被通知白筹。
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)
}
}
//調(diào)用 call.start() 方法會(huì)導(dǎo)致 call 的生命周期變化智末。如果連接成功谅摄,則標(biāo)記 action 為 fullfill。
call.start { [weak self] (success) in
guard let w = self else {
return
}
if success {
//這里填寫你們App內(nèi)打電話的邏輯系馆。送漠。
w.callManager.add(call: call)
//所有的Action只有調(diào)用了fulfill()之后才算執(zhí)行完畢。
action.fulfill()
} else {
action.fail()
}
}
}
//當(dāng)系統(tǒng)激活 CXProvider 的 audio session時(shí)由蘑,委托會(huì)被調(diào)用闽寡。這給你一個(gè)機(jī)會(huì)開始處理通話的音頻。
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
startAudio() //一定要記得播放鈴聲吶尼酿。爷狈。
}
至此,通過CallKit撥打電話的邏輯就完成了裳擎。你只要在自己App需要打電話的地方淆院,調(diào)用
CallKitManager.shared.startCall(handle: userName, videoEnabled: false)就行啦。句惯。但是有一點(diǎn)需要注意土辩,CallKit只有iOS 10以上支持,所以iOS 10以下的手機(jī)還是要支持你們原來打電話的邏輯抢野,像這樣:
if #available(iOS 10.0, *) {
CallKitManager.shared.startCall(handle:userName, videoEnabled: false)
} else {
//原來打電話的邏輯
}
然后當(dāng)你興沖沖的去用CallKit打電話的時(shí)候拷淘,卻發(fā)現(xiàn)彈出的是自己的通話頁面。指孤。启涯。T_T
但是此時(shí)你查看系統(tǒng)的通話記錄,應(yīng)該會(huì)發(fā)現(xiàn)通話記錄里面新增了一條從自己App打出去的記錄恃轩。這樣就說明CallKit撥打電話接入成功了结洼!
因?yàn)閮?nèi)容較多,分成了三篇文章叉跛,下一篇講如何接電話松忍,繼續(xù)完善這篇文章的代碼。
iOS CallKit與PushKit的集成(二)