CallKit框架详细解析(三) —— 基本使用(二)

版本记录

版本号 时间
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和应用现在都反映了正确的持续通话状态。

The ongoing call shown on the home screen, and the main screen of Hotline.

Ending the Call

接听电话会发现一个新问题:目前无法结束通话。 该应用程序将支持两种结束通话的方式 - 来自本机通话屏幕和应用内。

下图显示了两种情况下发生的情况:

注意步骤1a1b之间的区别。 当用户结束来自通话屏幕(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

此时,您的通话将结束。 锁定和主屏幕以及应用程序都不会报告任何正在进行的通话。

Hanging up now!

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)
}    

代码中有一个微妙的变化:当incomingfalse时,视图控制器将要求call manager启动传出呼叫。

这就是你需要打电话的全部内容。 是时候开始测试了! 在您的设备上构建并运行应用程序。 点击右侧角落的加号按钮开始新的通话,但这次请确保从segmented control中选择Outgoing

此时,您应该看到新呼叫call出现在列表中。 您还会根据当前的通话阶段看到不同的状态标签:

I can make calls now?!

Managing Multiple Calls

如果Hotline用户收到多个电话怎么办? 您可以通过先拨打拨出电话然后拨打来电并在来电进入之前按Home按钮来模拟此操作。此时,应用程序会向用户显示以下屏幕:

系统允许用户决定如何解决问题。 根据用户的选择,它会将多个操作组合到一个CXTransaction中。 例如,如果用户选择结束正在进行的呼叫并应答新呼叫,则系统将为前者创建CXEndCallAction,为后者创建CXStartCallAction。 这两个操作都将被包装到一个事务中并发送给providerprovider将单独处理它们。 如果您的应用已经知道如何满足各个要求,那么您的工作就完成了!

Implementing features without additional code!

您可以通过解决上面的方案来测试它。 通话清单将反映您的选择。 该应用程序一次只能处理一个音频会话。 如果您选择恢复通话,则另一个将自动暂停。


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:

FYI: That’s not a real number

恭喜! 您已经创建了一个利用CallKit提供第一方VoIP体验的应用程序!

如果您想了解有关CallKit的更多信息,请查看2016年WWDC会议230 - Session 230

后记

本篇主要讲述了CallKit框架基本使用,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容