版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.15 星期一 |
前言
苹果 iOS 10 新发布了一个新的框架CallKit,使第三方VOIP类型语音通话类APP有了更好的展现方式和用户体验的提升,接下来这几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. CallKit框架详细解析(一) —— 基本概览(一)
2. CallKit框架详细解析(二) —— 基本使用(一)
Test Flight
现在,构建并运行应用程序,并执行以下操作:
- 1) 点击右上角的加号按钮
(+)
。 - 2) 输入任意数字,确保在
segmented control
中选择Incoming
,然后点击Done
。 - 3) 锁定屏幕。 此步骤很重要,因为它是访问丰富的本机内部调用UI的唯一方法。
在几秒钟内,您将看到本机来电UI:
但是,只要您接听电话,您就会注意到UI仍然处于以下状态:
这是因为你仍然需要实现负责接听电话的部分。 返回Xcode,返回ProviderDelegate.swift
,并将以下代码添加到类扩展中:
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
// 1.
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 2.
configureAudioSession()
// 3.
call.answer()
// 4.
action.fulfill()
}
// 5.
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
startAudio()
}
以下是逐步细分:
- 1) 引用来自
call manager
,对应于应答呼叫的UUID
。 - 2) 该应用程序配置呼叫的音频会话。 系统以较高的优先级激活会话。
- 3)
answer()
表示该呼叫现在处于活动状态。 - 4) 处理动作时,重要的是失败或完成它。 假设在此过程中没有错误,您可以调用
fulfill()
来表示成功。 - 5) 一旦系统激活
provider
的音频会话,就会通知代理。 这是您开始处理呼叫音频的机会。
构建并运行应用程序,然后再次启动来电。 当您应答呼叫时,系统将成功转换为正在进行的呼叫状态。
如果您解锁手机,您会发现iOS和应用现在都反映了正确的持续通话状态。
Ending the Call
接听电话会发现一个新问题:目前无法结束通话。 该应用程序将支持两种结束通话的方式 - 来自本机通话屏幕和应用内。
下图显示了两种情况下发生的情况:
注意步骤1a
和1b
之间的区别。 当用户结束来自通话屏幕(1a)的呼叫时,系统自动将CXEndCallAction
发送给provider
。 但是,如果您想使用Hotline (1b)
结束通话,那么你需要做的是将操作包装到事务中并从系统请求。 一旦系统处理了请求,它就会将CXEndCallAction
发送回provider
。
CXProviderDelegate
虽然它支持结束调用,您的应用必须实现必要的CXProviderDelegate
方法才能工作。 打开ProviderDelegate.swift
并将以下实现添加到类扩展:
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
// 1.
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 2.
stopAudio()
// 3.
call.end()
// 4.
action.fulfill()
// 5.
callManager.remove(call: call)
}
下面就进行详细分解:
- 1) 首先从
call manager
获取对该call
的引用。 - 2) 当呼叫即将结束时,是时候停止处理呼叫的音频了。
- 3) 调用
end()
会更改call
的状态,允许其他类对新状态做出反应。 - 4) 此时,您将标记操作已完成。
- 5) 由于您不再需要呼叫,
call manager
可以处理它。
这会处理通话中的UI。 为了结束来自应用程序的调用,您需要扩展CallManager
。
Requesting Transactions
call manager
将与CXCallController
通信,因此它需要对实例的引用。 将以下属性添加到CallManager.swift
:
private let callController = CXCallController()
现在将以下方法添加到类中:
func end(call: Call) {
// 1.
let endCallAction = CXEndCallAction(call: call.uuid)
// 2.
let transaction = CXTransaction(action: endCallAction)
requestTransaction(transaction)
}
// 3.
private func requestTransaction(_ transaction: CXTransaction) {
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction successfully")
}
}
}
下面进行详细分解:
- 1) 创建
End call action
。 将调用的UUID
传递给初始化程序,以便稍后识别。 - 2) 将操作包装到事务中,以便将其发送到系统。
- 3) 从
call controller
调用request(_:completion:)
。 系统将请求provider
执行此事务,该事务将依次调用刚刚实现的委托方法。
最后一步是将操作连接到用户界面。 打开CallsViewController.swift
并在tableView(_:cellForRowAt :)
实现下面编写以下调用:
override func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath
) {
let call = callManager.calls[indexPath.row]
callManager.end(call: call)
}
当用户在行上调用滑动到删除时,应用程序将要求CallManager
结束相应的调用。
在您的设备上构建并运行项目,然后执行以下步骤:
- 1) 点击右侧角的加号按钮
(+)
。 - 2) 输入任意数字,确保在
segmented control
中选择Incoming
,然后点击Done
。 - 3) 在几秒钟内,您将收到来电。 一旦你回答,你应该看到它在UI上列为活动状态。
- 4) 在表示当前通话的行上向左滑动,然后点按
End
。
此时,您的通话将结束。 锁定和主屏幕以及应用程序都不会报告任何正在进行的通话。
Other Provider Actions
CXProviderDelegate的文档页面显示provider
可以执行的操作还有很多,包括静音和分组或设置保持的呼叫。 后者听起来像Hotline
的一个很好的功能。 为什么不立即实施呢?
当用户想要设置呼叫的保持状态时,应用程序将向provider
发送CXSetHeldCallAction
的实例。 实现相关的代理方法是你的工作。 打开ProviderDelegate.swift
并将以下实现添加到类扩展:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 1.
call.state = action.isOnHold ? .held : .active
// 2.
if call.state == .held {
stopAudio()
} else {
startAudio()
}
// 3.
action.fulfill()
}
此代码执行以下操作:
- 1) 获取对调用的引用后,根据操作的
isOnHold
属性更新其状态。 - 2) 根据状态,启动或停止处理呼叫的音频。
- 3) 标记动作已完成。
由于这也是用户启动的操作,因此您还需要扩展CallManager
类。 打开CallManager.swift
并在end(call :)
下面添加以下实现:
func setHeld(call: Call, onHold: Bool) {
let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
let transaction = CXTransaction()
transaction.addAction(setHeldCallAction)
requestTransaction(transaction)
}
代码与end(call :)
非常相似。 实际上,两者之间的唯一区别是这个将把一个CXSetHeldCallAction
实例包装到事务中。 该操作将包含呼叫的UUID
和保持状态。
现在是时候将此操作连接到UI。 打开CallsViewController.swift
并在文件末尾找到标有UITableViewDelegate
的类扩展。 将以下实现添加到类扩展,就在tableView(_:titleForDeleteConfirmationButtonForRowAt :)
下面:
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
let call = callManager.calls[indexPath.row]
call.state = call.state == .held ? .active : .held
callManager.setHeld(call: call, onHold: call.state == .held)
tableView.reloadData()
}
当用户点击一行时,上面的代码将更新相应call
的保持状态。
构建并运行应用程序并启动新的来电。 如果您点击呼叫的单元格,您会注意到状态标签将从Active
更改为On Hold
。
Handling Outgoing Calls
您要实施的最终用户启动操作是拨打电话。 打开ProviderDelegate.swift
并将以下实现添加到CXProviderDelegate
类扩展:
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let call = Call(uuid: action.callUUID, outgoing: true,
handle: action.handle.value)
// 1.
configureAudioSession()
// 2.
call.connectedStateChanged = { [weak self, weak call] in
guard
let self = self,
let call = call
else {
return
}
if call.connectedState == .pending {
self.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
} else if call.connectedState == .complete {
self.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
}
}
// 3.
call.start { [weak self, weak call] success in
guard
let self = self,
let call = call
else {
return
}
if success {
action.fulfill()
self.callManager.add(call: call)
} else {
action.fail()
}
}
}
当发出传出呼叫请求时,提供程序将调用此委托方法:
- 1) 使用来自
call manager
的呼叫UUID
创建呼叫Call
后,您必须配置应用的音频会话。 就像来电一样,此时您的责任仅是配置。 当调用provider(_:didActivate)
时,实际处理将在稍后开始。 - 2) 代理监视
Call
的生命周期。 它最初会报告传出呼叫已开始连接。 连接呼叫时,provider
代理也将报告该呼叫。 - 3) 在调用上调用
start()
会触发其生命周期更改。 成功连接后,可以将呼叫标记为已完成。
1. Starting the Call
现在provider
代理已准备好处理拨出呼叫,现在是时候教会应用程序如何制作一个。 打开CallManager.swift
并将以下方法添加到类中:
func startCall(handle: String, videoEnabled: Bool) {
// 1
let handle = CXHandle(type: .phoneNumber, value: handle)
// 2
let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
// 3
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction(action: startCallAction)
requestTransaction(transaction)
}
此方法将Start Start操作包装到CXTransaction中,并从系统请求它。
- 1) 由
CXHandle
表示的句柄可以指定句柄类型及其值。Hotline
支持电话号码处理,因此您也可以在这里使用它。 - 2)
CXStartCallAction
接收唯一的UUID
和句柄作为输入。 - 3) 通过设置操作的
isVideo
属性,指定呼叫是仅音频还是视频呼叫。
是时候将新action
连接到UI了。 打开CallsViewController.swift
并用以下内容替换以前的unwindForNewCall(_ :)
实现:
guard
let newCallController = segue.source as? NewCallViewController,
let handle = newCallController.handle
else {
return
}
let videoEnabled = newCallController.videoEnabled
let incoming = newCallController.incoming
if incoming {
let backgroundTaskIdentifier =
UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
AppDelegate.shared.displayIncomingCall(
uuid: UUID(),
handle: handle,
hasVideo: videoEnabled
) { _ in
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
} else {
callManager.startCall(handle: handle, videoEnabled: videoEnabled)
}
代码中有一个微妙的变化:当incoming
为false
时,视图控制器将要求call manager
启动传出呼叫。
这就是你需要打电话的全部内容。 是时候开始测试了! 在您的设备上构建并运行应用程序。 点击右侧角落的加号按钮开始新的通话,但这次请确保从segmented control
中选择Outgoing
。
此时,您应该看到新呼叫call
出现在列表中。 您还会根据当前的通话阶段看到不同的状态标签:
Managing Multiple Calls
如果Hotline
用户收到多个电话怎么办? 您可以通过先拨打拨出电话然后拨打来电并在来电进入之前按Home
按钮来模拟此操作。此时,应用程序会向用户显示以下屏幕:
系统允许用户决定如何解决问题。 根据用户的选择,它会将多个操作组合到一个CXTransaction
中。 例如,如果用户选择结束正在进行的呼叫并应答新呼叫,则系统将为前者创建CXEndCallAction
,为后者创建CXStartCallAction
。 这两个操作都将被包装到一个事务中并发送给provider
,provider
将单独处理它们。 如果您的应用已经知道如何满足各个要求,那么您的工作就完成了!
您可以通过解决上面的方案来测试它。 通话清单将反映您的选择。 该应用程序一次只能处理一个音频会话。 如果您选择恢复通话,则另一个将自动暂停。
Creating a Call Directory Extension
目录扩展(directory extension)
是CallKit
提供的新扩展点。它允许您的VoIP
应用程序:
- 将电话号码添加到系统的黑名单中。
- 通过电话号码或其他唯一标识信息(如电子邮件地址)识别来电。
当系统收到呼叫时,它将检查地址簿是否匹配。如果找不到,它还可以检查特定于应用程序的目录扩展。为什么不向Hotline
添加目录扩展?
在Xcode中,转到File ▸ New ▸ Target…
并选择Call Directory Extension
。将其命名为HotlineDirectory
,然后单击Finish
。 Xcode将自动创建一个新文件CallDirectoryHandler.swift
。在Project
导航器中找到它,看看里面有什么。
你会发现的第一个方法是beginRequest(with:)
。初始化您的扩展将调用此方法。如果有任何错误,扩展程序将通过调用requestFailed(for:withError :)
告诉主机应用程序取消扩展请求。它依赖于另外两种方法来构建特定于应用程序的目录。
addAllBlockingPhoneNumbers(to :)
将收集应阻止的所有电话号码。用以下内容替换其实现:
let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1234 ]
for phoneNumber in phoneNumbers {
context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
}
使用给定的电话号码调用addBlockingEntry(withNextSequentialPhoneNumber :)
会将其添加到黑名单列表中。 系统电话provider
不会显示来自黑名单的呼叫。
现在,看看addAllIdentificationPhoneNumbers(to :)
。 用下面的代码替换方法体:
let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1111 ]
let labels = [ "RW Tutorial Team" ]
for (phoneNumber, label) in zip(phoneNumbers, labels) {
context.addIdentificationEntry(
withNextSequentialPhoneNumber: phoneNumber,
label: label)
}
使用指定的电话号码和标签调用addIdentificationEntry(withNextSequentialPhoneNumber:label :)
将创建一个新的标识条目。 当系统从此号码接收呼叫时,呼叫UI将显示与用户匹配的标签。
Settings Setup
是时候测试你的新扩展了。 在您的设备上构建并运行Hotline
。 此时您的扩展程序可能尚未激活。 要启用它,请执行以下步骤:
- 1) 转到
Settings
应用 - 2) 选择
Phone
- 3) 选择
Call Blocking & Identification
- 4) 启用
Hotline
注意:如果您在让系统识别或使用您的扩展程序时遇到问题,请尝试终止该应用并重新启动它。 有时,iOS需要一些额外的帮助才能使用您的扩展程序
测试黑名单的呼叫很容易。 启动Hotline
并模拟来自号码1234
的来电。您会注意到系统没有报告任何内容。 实际上,如果你在ProviderDelegate的reportIncomingCall(uuid:handle:hasVideo:completion :)
的实现中放置一个断点,你会注意到reportNewIncomingCall(withupdate:completion :)
甚至会报告错误。
要测试识别呼叫,请再次启动Hotline
并模拟新call
。 这次输入数字1111
,您将看到以下调用UI:
恭喜! 您已经创建了一个利用CallKit
提供第一方VoIP
体验的应用程序!
如果您想了解有关CallKit
的更多信息,请查看2016年WWDC会议230 - Session 230。
后记
本篇主要讲述了CallKit框架基本使用,感兴趣的给个赞或者关注~~~