10 callkit ios_ios callkit - CSDN
  • CallKit iOS 教程

    2017-07-05 10:44:39
    原文:CallKit Tutorial for iOS 作者:József Vesza 译者:kmyhy 对 VoIP App 开发者来说,iOS 的支持并不友好。尤其是它的通知发送这一块,太糙了。你的 App 允许在后台,你唯一的选择就是使用常规的通知,这...

    原文:CallKit Tutorial for iOS
    作者:József Vesza
    译者:kmyhy

    对 VoIP App 开发者来说,iOS 的支持并不友好。尤其是它的通知发送这一块,太糙了。你的 App 允许在后台,你唯一的选择就是使用常规的通知,这也太容易搞丢了。和内置的、丰富的电话 UI 一比,突然你的 App 是如此的不和谐。

    幸好,苹果在 iOS 10 中推出了 CallKit,让这一切发生了改变!

    在这个教程中,你将通过编写一个 App 领略到 CallKit 的风采:

    • 通过系统服务监听来点和去电。
    • 用电话通讯录识别或拦截来电。

    注意:CallKit 无法在模拟器上运行。为了配合本教程,你必须使用一台装有 iOS 10.2 的 iPhone。

    开始

    从此处下载本教程的开始项目,然后解压缩。为了在设备上调试项目,你必须对代码进行签名。打开项目文件,在项目导航器中选择 Hotline。

    你需要修改 bundle ID。选中项目,在 General 窗口,找到 Identity 一栏。将 bundle ID 修改为其它:

    然后,找到 Signing 栏。从下拉框中选择你的开发团队(以我为例,是我自己的个人团队)。确保勾选上 Automatically manage signing。这允许 Xcode 自动创建 App 所用的 provisioning profile。

    运行 App 进行测试。

    目前 App 还没有什么内容,但你会在开始项目中发现几个源文件。它们大部分用于创建UI,处理用户交互,其中比较值得注意的是这两个类:

    • Call 类代表一个电话通话。这个类暴露了一些属性,用于识别呼叫(比如它的 UUID 或者回调),以及生命周期回调,什么时候用户开始、接听或挂起。
    • CallManager 维护了 App 中的呼出列表,拥有添加和移除方法。在本教程中,你会扩展这个类。

    CallKit 是什么?

    CallKit 是一个新框架,用于改善 VoIP 的体验,允许 App 和原生的 Phone UI 紧密集成,你的 App 将能够:

    • 调用原生的呼入界面,无论锁屏/不锁屏状态。
    • 从原生电话 App 的通讯录、个人收藏、最近通话中发起通话。

    本节中,你将学习 CallKit 的构成。下图显示了几个重要对象:

    在使用 CallKit 时,有两个主要的类:CXProvider和 CXCallController。分别介绍如下。

    CXProvider

    你的 App 使用 CXProvider 来将外部通知报告给系统。通常是外部事件,比如来电。

    当有事件发生,CXProvider 会创建一个 call update 来通知系统。什么是 call update?call update 用于封装新的或者改变了的和通话有关的信息。它用 CXCallUpdate 类来描述,这个类暴露了这些属性:呼入者姓名、是否是音频通话还是视频通话。

    当系统想通知 App 有收到一个事件时,它会以 CXAction 的形式通知。CXAction 是一个抽象类,表示电话的动作。针对不同 action,CallKit 会提供不同的 CXAction 实现。例如,呼出用 CXStartCallAction 来表示,CXAnswerCallAction 则用于接听呼入。Action 通过唯一的 UUID 来识别,它要么是 fail 要么是 fulfill。

    App 通过 CXProviderDelegate 和 CXProvider 打交道,这个协议定义了 CXProvider 的生命周期事件方法,以及来电 Action。

    CXCallController

    App 使用 CXCallController 来让系统知道用户发起的请求,比如“呼叫”动作。CXProvider 和 CXCallController 的最大不同在于:CXProvider 的工作是通知系统,而 CXCallController 则代表用户向用户发起请求。

    CXCallController 在发起请求时使用了事务。事务用 CXTransaction 来表示,它会包含一个或多个 CXAction 实例。CXCallCotroller 将事务发送给系统,如果一切正常,系统会响应对应的 action 给 CXProvider。

    理论还不少,但怎样使用它们呢?

    来电

    下图显示了来电的高度抽象的模型:

    1. 当来电呼入时,App 会创建一个 CXCallUpdate 然后通过 CXProvider 发送给系统。
    2. 系统会发布一个 incoming call 给它的服务。
    3. 当用户接听起电话时,系统会发送一个 CXAnswerCallAction 给 CXProvider。
    4. App 可以通过实现对应的 CXProviderDelegate 协议方法来回应这个动画。

    第一步是创建 CXProvider 的委托。

    回到 Xcode,在项目导航器中,选中 App 文件夹,点击菜单 File\New…,然后选择 iOS\Source\Swift File。名字命名为 ProviderDelegate,然后点 Create。

    在文件中添加代码:

    import AVFoundation
    import CallKit
    
    class ProviderDelegate: NSObject {
      // 1.
      fileprivate let callManager: CallManager
      fileprivate let provider: CXProvider
    
      init(callManager: CallManager) {
        self.callManager = callManager
        // 2.
        provider = CXProvider(configuration: type(of: self).providerConfiguration)
    
        super.init()
        // 3.
        provider.setDelegate(self, queue: nil)
      }
    
      // 4.
      static var providerConfiguration: CXProviderConfiguration {
        let providerConfiguration = CXProviderConfiguration(localizedName: "Hotline")
    
        providerConfiguration.supportsVideo = true
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.supportedHandleTypes = [.phoneNumber]
    
        return providerConfiguration
      }
    }

    这段代码解释如下:

    1. ProviderDelegate 需要和 CXProvider 和 CXCallController 打交道,因此保持两个对二者的引用。属性用 fileprivate 修饰,这样你就可以从同一个文件中的扩展中访问它们了。
    2. 用一个 CXProviderConfiguration 初始化 CXProvider,前者在后面会定义成一个静态属性。CXProviderConfiguration 用于定义通话的行为和能力。
    3. 为了能够响应来自于 CXProvider 的事件,你需要设置它的委托。这句代码会导致一个编译错误,因为 ProviderDelegate 还没有实现 CXProviderDelegate 协议。
    4. 在这个 App 中,CXProviderConfiguration 支持视频通话、电话号码处理,并将通话群组的数字限制为 1 个。更多的定制化,请参考 CallKit 文档

    在 providerConfiguration 下面,添加一个工具方法:

    func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)?) {
      // 1.
      let update = CXCallUpdate()
      update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
      update.hasVideo = hasVideo
    
      // 2.
      provider.reportNewIncomingCall(with: uuid, update: update) { error in
        if error == nil {
          // 3.
          let call = Call(uuid: uuid, handle: handle)
          self.callManager.add(call: call)
        }
    
        // 4.
        completion?(error as? NSError)
      }
    }

    这个工具方法允许 App 通过 CXProvider API 来报告一个来电。代码解释如下:

    1. 准备向系统报告一个 call update 事件,它包含了所有的来电相关的元数据。
    2. 调用 CXProvider 的reportIcomingCall(with:update:completion:)方法通知系统有来电。
    3. completion 回调会在系统处理来电时调用。如果没有任何错误,你就创建一个 Call 实例,将它添加到 CallManager 的通话列表。
    4. 调用 completion 块,如果它不为空的话。

    这个方法被其它类所调用,为了模拟来电呼入。

    接下来是实现协议方法。仍然在 ProviderDelegate.swift 文件中,声明一个新的扩展,实现 CXProviderDelegate:

    
    extension ProviderDelegate: CXProviderDelegate {
    
      func providerDidReset(_ provider: CXProvider) {
        stopAudio()
    
        for call in callManager.calls {
          call.end()
        }
    
        callManager.removeAllCalls()
      }
    }

    CXProviderDelegate 只实现一个 required 的方法,providerDidReset(_:)。当 CXProvider 被 reset 时,这个方法被调用,这样你的 App 就可以清空所有去电,会到干净的状态。在这个方法中,你会停止所有的呼出音频会话,然后抛弃所有激活的通话。

    现在 ProviderDelegate 提供了一个方法去报告来电,让我们来用用它!

    在项目导航器中选择 App 文件夹,打开 AppDelegate.swift。在类中添加一个新属性:

    lazy var providerDelegate: ProviderDelegate = ProviderDelegate(callManager: self.callManager)
    

    providerDelegate 已经整装待发!在 AppDelegate 中添加如下方法:

    func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)?) {
      providerDelegate.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion)
    }

    这个方法向其它类暴露 providerDelegate 的工具方法。

    最后一块拼图是将它和 UI 连接到一起。展开 UI/View Controllers 文件夹,打开 CallsViewController.swift,这是 App 主界面的控制器。找到空的 unwindSegueForNewCall(_:)方法,替换为如下代码:

    @IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
      // 1.
      let newCallController = segue.source as! NewCallViewController
      guard let handle = newCallController.handle else { return }
      let videoEnabled = newCallController.videoEnabled
    
      // 2.
      let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
      DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
        AppDelegate.shared.displayIncomingCall(uuid: UUID(), handle: handle, hasVideo: videoEnabled) { _ in
          UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
        }
      }
    }

    这段代码的大意是:

    1. 从 NewCallViewController 中读取这次通话的属性,它是 unwind segue 的起始 view controller。
    2. 用户可以在 action 结束之前挂起 App,这样 App 会使用后台任务。

    现在一切就绪,运行 App,进行如下操作:

    1. 点击右上角的 + 按钮;
    2. 输入任意数字,在 segmented 控件中选择 Incoming,然后点 Done。
    3. 锁屏。这步很重要,因为这是唯一能够访问完整的原生呼入界面的方法。

    几秒钟后,你会看到原生的呼入通话 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. 从 callManager 中获得一个引用,UUID 指定为要接听的动画的 UUID。
    2. 设置通话要用的 audio session 是 App 的责任。系统会以一个较高的优先级来激活这个 session。
    3. 通过调用 answer,你会表明这个通话现在激活。
    4. 在处理一个 CXAction 时,重要的一点是,要么你拒绝它(fail),要么满足它(fullfill)。如果处理过程中没有发生错误,你可以调用 fullfill() 表示成功。
    5. 当系统激活 CXProvider 的 audio session时,委托会被调用。这给你一个机会开始处理通话的音频。

    运行 App,再次开始呼入一个通话。当你接听时,系统会成功地变成去电状态。

    如果你解锁 iPhone,你会看到 iOS 和 App 都会显示出正确的呼出状态。

    结束通话

    接听通话会带来一个问题:没有办法结束通话。这个 App 将会支持两种结束通话的方式:从原生的通话界面,或者从 App 中进行结束。

    下图显示这两种结束通话的情况:

    注意第一步有所不同:当用户从通话界面结束通话(1a)时,系统会自动发送一个 CXEndCallAction 给 CXProvider。但是,如果你想用 Hotline App 来结束通话(1b),那么应该有你来将 CXAction 封装成 CXTransaction,然后请求系统。当系统处理完请求,它会发送 CXEndCallCation 给 CXProvider。

    不管哪种方法,你的 App 必须实现相应的 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. 从 callManager 获得一个 call 对象。
    2. 当 call 即将结束时,停止这次通话的音频处理。
    3. 调用 end() 方法修改本次通话的状态,以允许其他类和新的状态交互。
    4. 将 action 标记为 fulfill。
    5. 当你不再需要这个通话时,可以让 callManager 回收它。

    这只实现了从原生通话界面结束的情况。为了从 App 结束通话,你必须修改 CallManager。在项目导航器的 Call Management 文件夹下,打开 CallManager.swift。

    CallManager 需要和 CXCallController 通信,因此需要一个它的引用。添加属性:

    private let callController = CXCallController()
    

    在类中添加下列方法:

    Now add the following methods to the class:
    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. 先创建一个 CXEndCallAction。将通话的 UUID 传递给构造函数,以便在后面可以识别通话。
    2. 然后将 action 封装成 CXTransaction,以便发送给系统。
    3. 最后,调用 callController 的 request(_:completion:) 。系统会请求 CXProvider 执行这个 CXTransaction,这会导致你刚刚实现的委托方法被调用。

    最后是将代码和 UI 连接起来。打开 CallsViewController.swift,在 tableView(_:cellForRowAt:) 方法下面,添加代码:

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
      let call = callManager.calls[indexPath.row]
      callManager.end(call: call)
    }

    当用户在 cell 上使用轻扫-删除手势时,App 会请求 CallManager 结束对应的通话。

    运行 App,执行下列操作:

    1. 点击 + 按钮。
    2. 输入数字,选择 Incoming,点击 Done。
    3. 几秒钟后,你会接到一个来电。当你接听时,你会看到这个通话会在列表中出现。
    4. 在 cell 上向左轻扫,点 End。

    这时,通话结束。无论锁屏还是不锁屏,无论 App 是否在前台,这个 App 都会报告通话。

    其它提供者动作

    如果你看过 CXProviderDelegate 的文档,你会注意到 CXProvider 还会执行许多 CXAction,包括静音、群组或者设置呼叫等待(通话保持)。后面一个听起来不错,我们现在就来实现它。

    当用户在 cell 上轻扫-删除时,App 会请求 CallManager 去结束对应的通话。

    当用户想设置某个通话为“保持”状态,App 会发送一个 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. 获得 CXCall 对象之后,我们要根据 action 的 isOnHold 属性来设置它的 state。
    2. 根据状态的不同,分别进行启动或停止音频会话。
    3. 标记 action 为 fulfill。

    因为这个动作是用户发起的,我们还需要修改 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:) 非常像。事实上,二者唯一的不同是,后者封装在 transaction 中的是一个 CXSetHeldCallAction 对象。这个 action 包含了通话的 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()
    }

    当用户在某行上点击,上述代码会改变对应通话的保持状态。

    运行 App,开始新的呼入。如果你点击这个通话对应的行,你会注意到状态标签会从 Acitve 变成 On Hold。

    处理呼出通话

    最后还有一个用户发起的动作,需要我们实现,那就是呼出。打开 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 strongSelf = self, let call = call else { return }
    
        if call.connectedState == .pending {
          strongSelf.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
        } else if call.connectedState == .complete {
          strongSelf.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
        }
      }
      // 3.
      call.start { [weak self, weak call] success in
        guard let strongSelf = self, let call = call else { return }
    
        if success {
          action.fulfill()
          strongSelf.callManager.add(call: call)
        } else {
          action.fail()
        }
      }
    }

    当有呼出请求时,provider 会调用这个方法:

    1. 当我们用 UUID 创建出 Call 对象之后,我们就应该去配置 App 的音频会话。和呼入通话一样,你的唯一任务就是配置。真正的处理在后面进行,也就是在 provider(_:didActivate) 委托方法被调用时。
    2. delegate 会监听通话的生命周期。它首先会会报告的就是呼出通话开始连接。当通话最终连上时,delegate 也会被通知。
    3. 调用 call.start() 方法会导致 call 的生命周期变化。如果连接成功,则标记 action 为 fullfill。

    现在 ProviderDelegate 已经能够处理呼出了。接下来是让 App 进行一次呼出通话。

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

    这个方法将一个 CXStartCallAction 放到 CXTransaction 中,然后向系统发起请求。

    1. 一个 CXHandle 对象表示了一次操作,同时指定了操作的类型和值。Hotline App 支持对电话号码进行操作,因此我们在操作中指定了电话号码。

    2. 一个 CXStartCallAction 用一个 UUID 和一个操作作为输入。

    3. 你可以通过 action 的 isVideo 属性指定通话是音频还是视频。

    然后在 UI 中使用新方法。打开 CallsViewController.swift 将 unwindForNewCall(_:) 方法修改为:

    @IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
      let newCallController = segue.source as! NewCallViewController
      guard let handle = newCallController.handle else { return }
      let incoming = newCallController.incoming
      let videoEnabled = newCallController.videoEnabled
    
      if incoming {
        let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
        DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.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 时,view controller 会请求 CallManager 开始一次呼出通话。

    这就是打电话功能了。接下来我们测试一下!运行 App。点击 + 按钮,呼出一次通话,确保你选择了 segmented 控件中的 Outgoing。

    你应该能够在列表中看到新的通话。注意状态标签会根据当前通话的不同阶段变化:

    管理多个通话

    你很容易就会想到,Hotline 的用户会收到多个通话。你可以模拟一下,先呼出一次,再呼入一次,然后在呼入进来之前按下 Home 键。这时,App 会显示如下画面:

    系统让用户来决定如何处理这种问题。根据用户的选择,它会在一个 CXTransaction 中加入多个 action。例如,如果用户选择结束去电并接听来电,系统会先创建一个 CXEndCallActon,然后是一个CSStartCallAction。两个 action 都放在一个 transaction 中发送给 provider,provider 需要分别进行处理。因此,如果你的 App 能够分别对两个请求进行响应的话,那就不需要再多做什么了!

    你可以测试上面说的情况;通话列表会根据你的选择进行显示。App 一次只能处理一个音频会话。如果你选择恢复通话,另一个会自动变成保持通话状态。

    创建扩展通讯录

    通讯录扩展是 CallKit 提供的一个新功能。它允许你的 VoIP App:

    • 将号码添加到系统的黑名单。
    • 识别来电号码或者其它唯一识别标记,比如 email 地址。

    当系统收到来电,它会在通讯录中进行陪陪,如果没有找到结果,它会在 App 的扩展通讯录中查找。那就让我们在 Hotline 中添加一个扩展通讯录吧!

    返回 Xcode,点击菜单 File\New\Target… 然后选择 Call Directory Extension。Xcode 会自动创建一个新文件 CallDirectoryHandler.swift。在项目导航器中选中它,看一下的内容。

    第一个方法是 beginRequest(with:)。这个方法在扩展被初始化时调用。如果发生错误,扩展会告诉宿主 App 取消这次扩展请求(通过调用 cancelRequest(withError:)方法)。另外两个方法用于构建 App 的通讯录。

    addBlockingPhoneNumber(to:) 方法用于定义要阻塞的电话号码。修改这个方法为:

    private func addBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
      let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1234 ]
      for phoneNumber in phoneNumbers {
        context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
      }
    }

    以指定的号码调用 addBlockingEntry(withNextSequentialPhoneNumber:) 方法,将这个号码添加到黑名单。当某个号码被阻塞,系统电话 provider 不会显示任何来自这个号码的来电。

    然后是 addIdentificationPhoneNumbers(to:) 方法。将这个方法修改为:

    private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
      let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1111 ]
      let labels = [ "RW Tutorial Team" ]
    
      for (phoneNumber, label) in zip(phoneNumbers, labels) {
        context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
      }
    }

    将某个号码和 label 作为参数调用 addIdentificationEntry(withNextSequentialPhoneNumber:label:) 方法将创建一个新的 identification entry。当系统收到这个号码的来电时,电话 UI 上会显示这个 label 给用户。

    来测试一下。在设备上运行 App。但是你的扩展并没有被激活。你需要经过以下步骤来激活它:

    1. 打开设置程序。
    2. 选择电话。
    3. 选择来电阻止与身份识别
    4. 将 Hotline 开关打开

    注意:如果你无法让系统识别出你的扩展,请退出 App 并重新打开。有时候 iOS 在使用你的扩展时会有点问题。

    测试来电阻止其实很简单:点开 Hotline,以号码 1234 来进行一次呼入。你会注意到系统不会报告任何来电。事实上,你可以在 ProviderDelegate 的reportIncomingCall(uuid:handle:hasVideo:completion:) 方法中打一个断点,你会看到 reportNewIncomingCall 这句代码甚至会报错。

    要测试身份识别,再次运行 Hotline,模拟一次呼入,这次,号码输入 1111。你会看到如下的通话界面:

    恭喜!你创建了一个 App,用 CallKit 提供了原生的 VoIP 体验!:]

    结束

    你可以从这里下载最终完成的项目。

    如果你想学习更多关于 CallKit 的内容,请看 WWDC 2016 第 230 讲会议视频

    希望你喜欢这篇 CallKit 教程。有任何建议或问题,请在下面留言。

    展开全文
  • 很多VoIP的开发者发现,升级到Xcode9以后,原来的Voice over IP的选项消失了,需要自行去info.plist中添加App provides Voice over IP services。...这里我就来简单介绍一下如何集成CallKit与PushKit。
    很多VoIP的开发者发现,升级到Xcode9以后,原来的Voice over IP的选项消失了,需要自行去info.plist中添加App provides Voice over IP services。在某些时候,对传统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拨打电话接入成功了!


    接电话


    首先来讲一下如果接电话,来到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 {
                    // "对方已接通,请使用听筒接听"
                }
            }
        }
    }


    从系统通话记录中拨打App电话


    在文章的开头,我们已经导入了Intents.framework,下面开始对他进行操作。

    首先创建两个协议,目的是为了加几个属性:
    protocol StartCallConvertible {
        var startCallHandle: String? { get }
        var video: Bool? { get }
    }
    
    extension StartCallConvertible {
        var video: Bool? {
            return nil
        }
    }
    
    @available(iOS 10.0, *)
        protocol SupportedStartCallIntent {
        var contacts: [INPerson]? { get }
    }
    
    @available(iOS 10.0, *)
    extension INStartAudioCallIntent: SupportedStartCallIntent {}
    @available(iOS 10.0, *)
    extension INStartVideoCallIntent: SupportedStartCallIntent {}

    然后对NSUserActivity进行扩展,实现StartCallConvertible协议:
    extension NSUserActivity: StartCallConvertible {
    
        var startCallHandle: String? {
            if #available(iOS 10.0, *) {
                guard let interaction = interaction,
                        let startCallIntent = interaction.intent as? SupportedStartCallIntent,
                        let contact = startCallIntent.contacts?.first
                else {
                    return nil
                }
                return contact.personHandle?.value
            }
            return nil
        }
    
        var video: Bool? {
            if #available(iOS 10.0, *) {
                guard let interaction = interaction,
                    let startCallIntent = interaction.intent as? SupportedStartCallIntent
                else {
                    return nil
                }
    
                return startCallIntent is INStartVideoCallIntent
            }
            return nil
        }
    }

    回到AppDelegate中,实现代理方法:
    override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
        if #available(iOS 10.0, *) {
            guard let handle = userActivity.startCallHandle else {
                log.info("Callkit& Could not determine start call handle from user activity: \(userActivity)")
                return false
            }
    
            guard let video = userActivity.video else {
                log.info("Callkit& Could not determine video from user activity: \(userActivity)")
                return false
            }
            //如果当前有电话,需要根据自己App的业务逻辑判断
    
            //如果没有电话,就打电话,调用自己的打电话方法。
    
            return true
        }
        return false
    }

    至此,从通话记录中调到自己App打电话的功能就实现啦。
    展开全文
  • 苹果在iOS 10开放了系统电话权限,全新的Callkit框架能够让音视频的第三方应用获得系统级的通话体验,本次分享将主要介绍如何应用Callkit框架和一些适配经验。 下面是本期分享内容整理 大家好,我是来自腾讯...

    http://dev.qq.com/topic/58009392302e4725036142fc

    苹果在iOS 10开放了系统电话权限,全新的Callkit框架能够让音视频的第三方应用获得系统级的通话体验,本次分享将主要介绍如何应用Callkit框架和一些适配经验。

    下面是本期分享内容整理


    大家好,我是来自腾讯SNG的段定龙,目前负责QQ音视频iOS客户端的开发工作,很高兴今天和大家分享一下QQ电话适配iOS10 Callkit的经验。

    今天将从4个方面进行分享:

    1. Callkit概述
    2. Callkit框架
    3. 适配经验分享
    4. 更多资料

    1. Callkit 概述

    苹果在2016年的WWDC大会上推出了iOS10,提供了一系列更加开放的新特性,其中最吸引我们的就是Callkit,这个框架能够让第三方应用获得系统电话的权限以及体验。什么概念呢?上图吧。

    这个框架解决了VoIP通话的三个痛点:

    1. 提高网络通话的音频权限:避免在通话过程中被传统电话无脑打断,更顺畅!
    2. 可以使用系统电话的UI界面:QQ电话真正地变成了“电话”!
    3. 可以使用系统服务,丰富了入口:比如锁屏的时候可以直接接听,通过系统通话沉淀发起和Siri唤起通话等

    不得不给苹果点个赞,需求已宣讲,下面我们来看看怎么实现如此炫酷的体验。

    2. Callkit 框架

    2.1 整体结构

    首先得介绍一下Callkit的框架。他分为三大模块:VoIP,CallCenter和来电屏蔽,要实现上述功能我们只需要关注Voip模块。Voip模块里主要有两个类:CXProvider和CXCallController。

    CXProvider可以理解为处理系统电话界面有关的逻辑,比如来电呼起系统电话界面或者将用户在系统电话界面上的操作通知给App。 CXCallController则是将用户在App界面上的操作通知给系统。

    2.2 四个主要流程的接口模块使用

    更具体地,网络通话适配Callkit主要包含四个流程:收到来电主动通知Callkit、用户在Callkit界面点击接听、用户在手Q界面点击挂断、用户在系统通讯录发起新的通话。下面将通过四个流程来介绍CXProvider、CXCallController、INIntent事件的使用,举一反三。

    首先我们看最简单的收到来电主动通知Callkit:

    收到服务器信令通知后只需要调用CXProvider的reportNewIncomingCall就可以了。调用过后系统通讯录会自动沉淀,系统电话界面会展示。

    图中的setCategory是为了避免出现无声问题,这个在后面会进行解释。

    然后是用户在Callkit界面点击接听,这里的流程通用于用户对Callkit的操作回调:

    用户点击接听后,我们会受到CXAnswerCallAction的回调,只需要在这里面添加App原来的音视频通话逻辑,再调用fulfill,整个流程就完成了。

    再然后是用户在App内点击挂断

    这时候我们需要添加一个CXEndCallAction到CXTransaction并调用requestTransaction请求执行,之后的流程与Callkit界面点击接听类似,收到CXEndCallAction回调,执行相应逻辑,调用fulfill完成流程。所有用户在app内的操作都以这种方式通知Callkit。

    最后我们来看一下如何从App外部发起,以系统通讯录为例子(Siri其实是一样样的)

    用户在点击系统通讯录的沉淀后,我们会收到系统事件通知(INStartAudioCallIntent或者INStartVideoCallIntent),然后就类似于用户在App内点击挂断的流程,只不过这次换成发起了,添加CXStartCallAction到CXTransaction并调用requestTransaction请求执行,收到CXStartCallAction的回调,执行相应逻辑后调用fulfill完成流程。

    以上便是网络通话中主要的4个场景流程,不知道大家对CXProvider和CXCallController的功能和使用场景是否已经有一个大致的了解。最后用一张图来再解释一下:

    适配过总的结构如图所示,系统界面由系统自己控制,我们没有办法直接对其进行操作,这里有点坑,有很多苹果的BUG无法避免,我们需要CXCallController去通知系统更新,并通过CXProvider的回调处理在系统界面上的操作。

    回顾了一下整个Callkit的架构后,下面将分享一些适配时候的经验,包括ID的对应和无声问题的处理

    3. 手Q适配框架及经验

    3.1 适配手Q音视频架构

    Callkit的架构里有两个ID标志,UUID和CXHandle,前者是用于表示每一次通话,后者则是用于标识具体的用户,比如reportNewIncomingCall的时候我们需要新的UUID去标识这次通话,而在系统通讯录沉淀的时候,则使用CXHandle区分用户。

    QQ号码是一套独立的ID体系,区别于手机号,所以我们需要定义特殊的CXHandle字符串,并将UUID,CXHandle和QQ自己的AVID联系起来,统一管理。

    3.2 无声问题的坑

    整个适配过程中,我们遇到最大的问题就是出现通话无声问题,由于没有任何文档,在无数次的尝试后得出结论,苹果对于Callkit和App的音频接口调用顺序有严格的要求,如果不按照一下顺序来调用会出现无声问题甚至Crash:

    稍微给大伙儿一点时间,看看这个图

    图中不同颜色代表不同的流程,系统音频模块(AVAudioSession)分为六个操作:

    • 初始化(AudioUnitInit)
    • 去初始化(AudioUnitUninit)
    • 通知激活(didActivateSession)
    • 开启音频(AudioUnitStart)
    • 通知结束(didDeativateSession)
    • 关闭音频(AudioUnitStop)

    重点其实就两个:

    1. 在流程开始前setCategory为PlayAndRecord
    2. 调用音频模块函数的时机:

    发起通话: Callkit回调 -> 初始化 -> fulfill -> 通知激活 -> 开启音频结束通话: Callkit回调 -> fulfill -> 通知结束 -> 关闭音频 -> 去初始化

    4. 结语

    最后提一下Pushkit通道的使用可以保证用户杀进程或者退后台了,依然可以后台唤起进程,完成通话,不过这不是今天的重点,就带过了。

    由于苹果对整个架构真的没有什么文档解释,所有的工作都是在适配的过程中进行摸索,每个beta版本的接口都有所变动,太细节性的东西今天就不一一介绍了。

    当然我们可以从一下途径获得更多资料(聊胜于无)

    官方文档

    https://developer.apple.com/reference/callkit

    WWDC2016视频介绍

    https://developer.apple.com/videos/play/wwdc2016/230/

    WWDC2016演示文档

    http://devstreaming.apple.com/videos/wwdc/2016/230b83wfxc7m69dm90q/230/230_enhancing_voip_apps_with_callkit.pdf

    SpeakerBox: Using Callkit to create a VoIP app(9.13有更新的版本)

    https://developer.apple.com/library/prerelease/content/samplecode/Speakerbox/Introduction/Intro.html

    今天的分享讲到这里就结束拉,谢谢大家,如果有任何问题,欢迎大家提出来。


    展开全文
  • CallKit 是一个iOS10新框架,用于改善 VoIP 的体验,允许 App 和原生的 Phone UI 紧密集成,你的 App 将能够: 调用原生的呼入界面,无论锁屏/不锁屏状态。 从原生电话 App 的通讯录、个人收藏、最近通话中发起...

     

    CallKit 是一个iOS10新框架,用于改善 VoIP 的体验,允许 App 和原生的 Phone UI 紧密集成,你的 App 将能够:

    • 调用原生的呼入界面,无论锁屏/不锁屏状态。
    • 从原生电话 App 的通讯录、个人收藏、最近通话中发起通话。
    • 通过系统服务监听来电和去电。
    • 用电话通讯录识别或拦截来电。

    CallKit 通话流程如下图:

     

    ---------------- 用户行为传递给系统 --------------- 

    CXCallController类 :程序向系统传递信息

    - (instancetype)init; //在私有串行队列创建.
    - (instancetype)initWithQueue:(dispatch_queue_t)queue;   //指定队列里创建.默认主线程 //指定使用


    @property (nonatomic, readonly, strong) CXCallObserver *callObserver;    //电话信息观察者
    {
    CXCallObserver类  :随时捕获电话信息的更新
    一个方法:- (void)setDelegate:(nullable id<CXCallObserverDelegate>)delegate queue:(nullable dispatch_queue_t)queue;
    一个代理:- (void)callObserver:(CXCallObserver *)callObserver callChanged:(CXCall *)call;
    一个属性:@property (nonatomic, readonly, copy) NSArray<CXCall *> *calls;//返回当前呼叫列表


    CXCall类 :电话信息类

    UUID  //唯一通过这个ID可以定位到特定时刻的特定号码的来电信息

    outgoing    // (bool)是否是自己拨打的

    onHold   //电话是否挂起

    hasConnected    //电话是否接通

    hasEnded    //电话是否结束
    }


    //以下三个方法:将CXTransaction传递给系统,如果error为空,将调起CXProvider类的-provider:executeTransaction: 代理方法
    - (void)requestTransaction:(CXTransaction *)transaction completion:(void (^)(NSError *_Nullable error))completion;
    - (void)requestTransactionWithActions:(NSArray<CXAction *> *)actions completion:(void (^)(NSError *_Nullable error))completion API_AVAILABLE(ios(11.0));
    - (void)requestTransactionWithAction:(CXAction *)action completion:(void (^)(NSError *_Nullable error))completion API_AVAILABLE(ios(11.0));

    CXTransaction类 :用户的任何操作(CXAction)被包装成CXTransaction,然后丢给CXCallController通知系统
    @property (nonatomic, readonly, copy) NSUUID *UUID;     //获取唯一标识
    @property (nonatomic, readonly, assign, getter=isComplete) BOOL complete;  //所有的CXAction是否已经完成
    @property (nonatomic, readonly, copy) NSArray<__kindof CXAction *> *actions;   //获取CXAction列表

    - (instancetype)initWithActions:(NSArray<CXAction *> *)actions;    //初始化CXTransaction  // 指定使用
    - (instancetype)initWithAction:(CXAction *)action;
    - (void)addAction:(CXAction *)action;  //添加CXTransaction


    CXAction类:电话操作载体类

    @property (nonatomic, readonly, copy) NSUUID *UUID;  // 唯一标示

    @property (nonatomic, readonly, assign, getter=isComplete) BOOL complete;  // 不论失败成功 是否完成

    @property (nonatomic, readonly, strong) NSDate *timeoutDate;  // action未完成的超时时间

    - (instancetype)init;  //指定初始化方法

    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // 指定初始化方法

    - (void)fulfill; // action已经完成(在当前时间)

    - (void)fail;

    CXCallAction类 :CXAction的子类,以下类的父类

    • CXStartCallAction类 :电话开始 

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID handle:(CXHandle *)handle;

    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID; // 不可用

    @property (nonatomic, copy, nullable) NSString *contactIdentifier;  // 呼叫接收方的标识符

    @property (nonatomic, getter=isVideo) BOOL video; // 是否是视频电话

    - (void)fulfillWithDateStarted:(NSDate *)dateStarted; // 指明电话的开始时间

    @property (nonatomic, copy) CXHandle *handle;

    {

                     CXHandle类

                  @property (nonatomic, readonly) CXHandleType type;

                  @property (nonatomic, readonly, copy) NSString *value;

                 - (instancetype)initWithType:(CXHandleType)type value:(NSString *)value NS_DESIGNATED_INITIALIZER;

                 - (instancetype)init NS_UNAVAILABLE;

                 - (BOOL)isEqualToHandle:(CXHandle *)handle

      }

    • CXAnswerCallAction类 :接听

    - (void)fulfillWithDateConnected:(NSDate *)dateConnected;  // 指明一个时间表明电话已经接通 

    • CXSetHeldCallAction类 :暂停

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID onHold:(BOOL)onHold; // 指定

    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // 指定

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID; // 不可用

    @property (nonatomic, getter=isOnHold) BOOL onHold; //是否被hold

    • CXSetMutedCallAction类 :静音

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID muted:(BOOL)muted;

    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID; // 不可用

    @property (nonatomic, getter=isMuted) BOOL muted;

    • CXSetGroupCallAction类 :群组电话

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID callUUIDToGroupWith:(nullable NSUUID *)callUUIDToGroupWith;

    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;

    - (instancetype)initWithCallUUID:(NSUUID *)callUUID;   // 不可用

    @property (nonatomic, copy, nullable) NSUUID *callUUIDToGroupWith;

    • CXPlayDTMFCallAction类 :双频多音功能
    • CXEndCallAction类 :挂断或拒接

    - (void)fulfillWithDateEnded:(NSDate *)dateEnded; // 指明一个end时间表完成



    -------------------- 系统信息传递给用户 --------------------- 

     


    CXProvider类
    //初始化一个CXProvider,应该定义成单利
    - (instancetype)initWithConfiguration:(CXProviderConfiguration *)configuration;   //指定使用
    {
    CXProviderConfiguration类
    @property (nonatomic, readonly, copy) NSString *localizedName; //获取系统来电页面显示的app名称和系统通讯记录的信息
    @property (nonatomic, strong, nullable) NSString *ringtoneSound; //来电铃声(在app的bundel中的)
    @property (nonatomic, copy, nullable) NSData *iconTemplateImageData; //锁屏接听时系统界面右下角app图标,要40x40大小
    @property (nonatomic) NSUInteger maximumCallGroups;    //最大通话组,默认2
    @property (nonatomic) NSUInteger maximumCallsPerCallGroup;  //每个通话组的最大呼叫次数,默认 5
    @property (nonatomic) BOOL includesCallsInRecents API_AVAILABLE(ios(11.0));   //每个通话结束是否显示在系统最近通话列表,默认YES
    @property (nonatomic) BOOL supportsVideo;  //是否支持视频,默认NO
    @property (nonatomic, copy) NSSet<NSNumber *> *supportedHandleTypes;   //CXHandleType
    - (instancetype)initWithLocalizedName:(NSString *)localizedName;  //指定使用
    }
    //设置代理,queue一般直接指定为nil,即在main线程执行callback
    - (void)setDelegate:(nullable id<CXProviderDelegate>)delegate queue:(nullable dispatch_queue_t)queue;

     

    重要流程:

    1、来了VoIP电话后,让系统按照它的配置弹出一个系统来电界面。UUID:每次随机生成,其方法是:

    - (void)reportNewIncomingCallWithUUID:(NSUUID *)UUID update:(CXCallUpdate *)update completion:(void (^)(NSError *_Nullable error))completion;

    {
    CXCallUpdate类
    @property (nonatomic, copy, nullable) CXHandle *remoteHandle;   //通话对方的Handel信息
    @property (nonatomic, copy, nullable) NSString *localizedCallerName;   //对方的名字,可以设置为app注册的昵称
    @property (nonatomic) BOOL supportsHolding;   //通话过程中再来电,是否支持保留并接听
    @property (nonatomic) BOOL supportsGrouping;   //通话是否可以加入一个群组
    @property (nonatomic) BOOL supportsUngrouping;  //通话当被加入群组时,是否可退出
    @property (nonatomic) BOOL supportsDTMF;  //是否支持键盘拨号
    @property (nonatomic) BOOL hasVideo;   //本次通话是否有视频
    }


    2、报告系统一个通话更新。动态更改provider的配置信息CXCallUpdate,比如拨打方,开始没有配置通话的界面,就可以在通话开始时更新这些配置信息

    - (void)reportCallWithUUID:(NSUUID *)UUID updated:(CXCallUpdate *)update;

    3、外拨电话时
    //通话连接时
    - (void)reportOutgoingCallWithUUID:(NSUUID *)UUID startedConnectingAtDate:(nullable NSDate *)dateStartedConnecting;
    //通话连接上
    - (void)reportOutgoingCallWithUUID:(NSUUID *)UUID connectedAtDate:(nullable NSDate *)dateConnected;

    4、报告系统通话结束的时间和原因
    - (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;


    @property (nonatomic, readwrite, copy) CXProviderConfiguration *configuration; //配置

    - (void)invalidate;   //让provider停止(所有存在的通话都被标记为失败结束)

    @property (nonatomic, readonly, copy) NSArray<CXTransaction *> *pendingTransactions;  //未完成的CXTransaction列表

    - (NSArray<__kindof CXCallAction *> *)pendingCallActionsOfClass:(Class)callActionClass withCallUUID:(NSUUID *)callUUID;

    --------- 代理 ---------
    //必须通过清除所有内部呼叫状态(断开通信信道、释放网络资源等)来响应此回调
    - (void)providerDidReset:(CXProvider *)provider;

    @optional
    //provider创建好以后触发
    - (void)providerDidBegin:(CXProvider *)provider;


    //是否执行一个transaction
    - (BOOL)provider:(CXProvider *)provider executeTransaction:(CXTransaction *)transaction;

    // 如果 provider:executeTransaction:error: 返回 NO, 下面的方法按逻辑执行
    //拨打方成功发起一个通话后触发
    - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;
    //接听方成功接听一个电话时触发
    - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action;
    //接听方拒接电话或者双方结束通话时触发
    - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action;
    //当点击系统通话界面的 暂停 按钮时,会触发
    - (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action;
    //当点击系统通话界面的 静音 按钮时,会触发
    - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action;
    //当点击系统通话界面的 添加通话 按钮时,会触发
    - (void)provider:(CXProvider *)provider performSetGroupCallAction:(CXSetGroupCallAction *)action;
    //
    - (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action;
    //通话响应超时,强制通话结束
    - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action;
    //audio session activation state改变时发起
    - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession;
    - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession;


    ------------------- app extension 相关 -------------------


    CXCallDirectoryManager类
    //获取app extension的授权
    identifier:app extension的bundleID
    enabledStatus:授权状态:未授权-授权-不知道
    - (void)getEnabledStatusForExtensionWithIdentifier:(NSString *)identifier completionHandler:(void (^)(CXCallDirectoryEnabledStatus enabledStatus, NSError *_Nullable error))completion;
    //需要实时更新 
    - (void)reloadExtensionWithIdentifier:(NSString *)identifier completionHandler:(nullable void (^)(NSError *_Nullable error))completion;

    CXCallDirectory类  
    一个属性:CXCallDirectoryPhoneNumber //可以标识一个电话号码数组

    CXCallDirectoryExtensionContext类
    两个功能:
    1>将号码添加(移除)到黑名单
    2>给一个号码添加(移除)一个标识。例如腾讯手机管家中的“腾讯手机管家:骚扰电话”
    一个代理:添加黑名单或号码标识出错时发起
    - (void)requestFailedForExtensionContext:(CXCallDirectoryExtensionContext *)extensionContext withError:(NSError *)error

    CXCallDirectoryProvider类
    一个方法:- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context;
    它基于NSExtensionRequestHandling协议,由Extention实现


    ------------------ 错误信息类 ----------------------

    CXError类 :错误信息类

     

     

     

    参考文章:

    https://www.jianshu.com/p/305bd923c1ae

    https://www.cnblogs.com/viviwu/p/5942483.html

    https://blog.csdn.net/kmyhy/article/details/74388913

    https://blog.csdn.net/zhaochen_009/article/details/53410288?locationNum=12&fps=1

     

     

    -- NORMAL --

    -- NORMAL --

    -- NORMAL --
    展开全文
  • CallKit 这个开发框架,能够让语音或视讯电话的开发者将 UI 界面整合在 iPhone 原生的电话 App 中.将允许开发者将通讯 App 的功能内建在电话 App 的“常用联络资讯”,以及“通话记录”,方便用户透过原生电话 App,...

    CallKit 这个开发框架,能够让语音或视讯电话的开发者将 UI 界面整合在 iPhone 原生的电话 App 中.将允许开发者将通讯 App 的功能内建在电话 App 的“常用联络资讯”,以及“通话记录”,方便用户透过原生电话 App,就能直接取用这些第三方功能;允许用户在通知中心就能直接浏览并回覆来电,来电的画面也将整合在 iOS 原生的 UI 里,总体来说,等于让 iOS 原本单纯用来打电信电话的“电话”功能,能够结合众多第三方语音通讯软件,具备更完整的数码电话潜力。CallKit 也拓展了在 iOS 8 就出现的 App Extensions 功能,可以让用户在接收来电时,在原生电话 App 中就透过第三方 App 辨识骚扰电话(例如诈骗).

    如何创建一个call项目?

    出现下图结构说明创建成功:

    来电提醒功能

    通过extension模板,创建CallDirectoryExtension
    用到的方法:

    //开始请求的方法,在打开设置-电话-来电阻止与身份识别开关时,系统自动调用
    - (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context;
    //添加黑名单:根据生产的模板,只需要修改CXCallDirectoryPhoneNumber数组,数组内号码要按升序排列
    - (BOOL)addBlockingPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context;
    // 添加信息标识:需要修改CXCallDirectoryPhoneNumber数组和对应的标识数组;CXCallDirectoryPhoneNumber数组存放的号码和标识数组存放的标识要一一对应,CXCallDirectoryPhoneNumber数组内的号码要按升序排列
    - (BOOL)addIdentificationPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context;
    设置

    实现流程

    在打开设置里的开关后,系统会调用beginRequest方法,在这个方法内部会调用添加黑名单和添加信息标识的方法,添加成功后,再调用completeRequestWithCompletionHandler方法通知系统;

    代码的实现:

    • sms:或者是sms://:发送短信;
    • tel: 或者是tel://:打电话
    • telprompt:或者是 telprompt://: 打电话;
    • mailto:发送邮件;
    • http:或者是 http://: 浏览网址;
    打电话的按钮
        NSMutableString * str=[[NSMutableString alloc] initWithFormat:@"tel:%@",self.noTextField.text];
        [self.callWebview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:str]]];
        if (!self.callWebview.subviews) {
            [self.view addSubview:_callWebview];
        }
    检查授权:
     CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
        // 获取权限状态
        [manager getEnabledStatusForExtensionWithIdentifier:@"com.tq.cccccccccalldemo.CallDirectoryExtension" completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
            if (!error) {
                NSString *title = nil;
                if (enabledStatus == CXCallDirectoryEnabledStatusDisabled) {
                    /*
                     CXCallDirectoryEnabledStatusUnknown = 0,
                     CXCallDirectoryEnabledStatusDisabled = 1,
                     CXCallDirectoryEnabledStatusEnabled = 2,
                     */
                    title = @"未授权,请在设置->电话授权相关权限";
                }else if (enabledStatus == CXCallDirectoryEnabledStatusEnabled) {
                    title = @"授权";
                }else if (enabledStatus == CXCallDirectoryEnabledStatusUnknown) {
                    title = @"不知道";
                }
                UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"提示"
                                                                               message:title
                                                                        preferredStyle:UIAlertControllerStyleAlert];
    
                UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {}];
    
                [alert addAction:defaultAction];
                [self presentViewController:alert animated:YES completion:nil];
            }else{
                UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"提示"
                                                                               message:@"有错误"
                                                                        preferredStyle:UIAlertControllerStyleAlert];
    
                UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {}];
    
                [alert addAction:defaultAction];
                [self presentViewController:alert animated:YES completion:nil];
            }
        }];
    CallDirectoryHandler文件的实现方法:

    注意:电话号码前要加区号:+86;电话号码需要升序排列(Numbers must be provided in numerically ascending order.)

    /**
     添加黑名单:无法接通
     */
    - (BOOL)addBlockingPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context
    /**
     添加信息标识
     */
    - (BOOL)addIdentificationPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context

    说明:将项目运行到真机之后,还需要在“设置->电话”设置应用的权限;

    项目基本可以达到需求了!黑名单的号码打进来是在通话中,标记的号码显示标记的名字;

    标记的号码

    这时你会发现你只有第一次运行项目的号码设置才起作用,或者是去设置里面重新授权;显然这是不行的;我们需要实时更新号码:

        CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
        [manager reloadExtensionWithIdentifier:@"com.tq.cccccccccalldemo.CallDirectoryExtension" completionHandler:^(NSError * _Nullable error) {
            if (error == nil) {
                UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"提示"
                                                                               message:@"更新成功"
                                                                        preferredStyle:UIAlertControllerStyleAlert];
    
                UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {}];
    
                [alert addAction:defaultAction];
                [self presentViewController:alert animated:YES completion:nil];
            }else{
                UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"提示"
                                                                               message:@"更新失败"
                                                                        preferredStyle:UIAlertControllerStyleAlert];
    
                UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {}];
    
                [alert addAction:defaultAction];
                [self presentViewController:alert animated:YES completion:nil];
            }
        }];
    在使用reload时容易出现的问题:
    1:出现 Error Domain=com.apple.CallKit.error.calldirectorymanager Code=1 "(null)"       CXErrorCodeCallDirectoryManagerErrorNoExtensionFound 该错误可能出现的原因是identifier   设置的不对 注意不要使用app groups 使用的是Call Directory Extension 的identifier

      2:出现 Error Domain=com.apple.CallKit.error.calldirectorymanager Code=4 "(null)" CXErrorCodeCallDirectoryManagerErrorDuplicateEntries该错误可能出现的原因是:存入的手机号 有重复 检查后去除重复号码

    3:出现 Error Domain=com.apple.CallKit.error.calldirectorymanager Code=6 "(null)" CXErrorCodeCallDirectoryManagerErrorExtensionDisabled该错误可能出现的原因是:未开启权限 在设置中->电话->来电阻止与身份识别 开启就行






    展开全文
  • 前不久苹果推出的callkit framework简直就是voip类应用的福音啊,一下把应用的体验提升到了系统级别的高度,加上之前推出的pushkit,形成了一套完整的voip解决方案。正好我们的产品就是一个类voip应用,当然要把...
  • CallKit 这个开发框架,能够让语音或视讯电话的开发者将 UI 界面整合在 iPhone 原生的电话 App 中.将允许开发者将通讯 App 的功能内建在电话 App 的“常用联络资讯”,以及“通话记录”,方便用户透过原生电话 App,...
  • 由于目前开发的应用是一款VoIP应用,需要接入iOS10新框架CallKit,由于这个框架在官网也只有各种方法,没有具体的说明,不过提供了一个swift版本的Demo作为参考, 官网Demo链接地址:官网Demo链接  使用CallKit...
  • iOS10适配之 CallKit

    2017-03-29 09:58:25
    公司产品的核心功能是VoIP语/视频通话,为了与时俱进,就要适配iOS最新的CallKit。关于CallKit的介绍我就不详述了,大家可以去看看iOS开发文档、WWDC或者直接Google。 总的来说,CallKit有三大优势: 1.提供...
  • 所以我曾经着手的项目被要求在iOS 10以上的系统实现对CallKit框架的使用,于是研究了大半月,总算初步搞定了一些功能的实现. 先强调一点,要想实现远程推送能拉起CallKit界面,首先必须要集成苹果在iOS 8推出的推送...
  • 为什么80%的码农都做不了架构师?>>> ...
  • 苹果在WWDC2016推出了iOS10系统新功能CallKit,可以调起系统的电话接听页,配合iOS8推出的PushKit使用,形成了一套完整的VoIP解决方案。这篇文章...
  • CallKit 这个开发框架,是在iOS10新增开放,提供了很多关于IOS电话功能权限, 其中可以进行电话号码的标示以及黑名单拦截功能 创建这个功能的流程如下
  • 近期 iOS 10 新特性中萌生出了新的 Callkit,故此对之前的电话状态监听 - iOS进行更新,特此针对 iOS 10 的新特性特此更新,顺便优化一下项目中的方法; 首先,通过项目中声明参数和方法实现中的提示信息可以得知...
  • 最近发现好多吃瓜问CallKit的VoIP开发适配,对iOS10的新特性开发和适配也在上个月完成,接下来就分享一下VoIP应用如何使用CallKit后台、锁屏接听和号码识别功能。 一、首先使用CallKit能做什么:(一句话,不仅让...
  • iOS10 CallKit简单开发

    2019-06-15 15:43:38
    2019独角兽企业重金招聘Python工程师标准>>> ...
  • CallKit 这个开发框架,能够让语音或视讯电话的开发者将 UI 界面整合在 iPhone 原生的电话 App 中.将允许开发者将通讯 App 的功能内建在电话 App 的“常用联络资讯”,以及“通话记录”,方便用户透过原生电话 App,...
  • CallKit 是融云音视频通话功能的 UI 界面 SDK。包含了单人、多人音视频通话的界面的各种场景和功能。您可以快速的集成 CallKit 来实现丰富的音视频通话界面,并进行自己的 UI 定制开发。同时我们开源了 CallKit,您...
  • 黑客技术点击右侧关注,了解黑客的世界!Java开发进阶点击右侧关注,掌握进阶之路!Python开发点击右侧关注,探讨技术话题!作者丨littleliang链接:https://jueji...
1 2 3 4 5 ... 13
收藏数 253
精华内容 101
关键字:

10 callkit ios