原文: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 教程。有任何建议或问题,请在下面留言。

CallKit iOS 教程的更多相关文章

  1. 最详细在Windows安装Xamarin.iOS教程

    最详细在Windows安装Xamarin.iOS教程 来源:http://www.cnblogs.com/llyfe2006/articles/3098280.html 本文展示了如何设立Xamari ...

  2. Xamarin iOS教程之键盘的使用和设置

    Xamarin iOS教程之键盘的使用和设置 Xamarin iOS使用键盘 在文本框和文本视图中可以看到,当用户在触摸这些视图后,就会弹出键盘.本节将主要讲解键盘的输入类型定义.显示键盘时改变输入视 ...

  3. Xamarin iOS教程之显示和编辑文本

    Xamarin iOS教程之显示和编辑文本 Xamarin iOS显示和编辑文本 在一个应用程序中,文字是非常重要的.它就是这些不会说话的设备的嘴巴.通过这些文字,可以很清楚的指定这些应用程序要表达的 ...

  4. Xamarin iOS教程之视图显示图像

    Xamarin iOS教程之视图显示图像 Xamarin iOS显示图像 在主视图中显示一个图像,可以让开发者的应用程序变的更有趣,例如,在一些应用程序开始运行时,都会通过图像来显示此应用程序的玩法或 ...

  5. Xamarin iOS教程之使用按钮接接收用户输入

    Xamarin iOS教程之使用按钮接接收用户输入 Xamarin iOS使用按钮接接收用户输入 按钮是用户交互的最基础控件.即使是在iPhone或者iPad中,用户使用最多操作也是通过触摸实现点击. ...

  6. Xamarin iOS教程之添加和定制视图

    Xamarin iOS教程之添加和定制视图 Xamarin iOS用户界面——视图 在iPhone或者iPad中,用户看到的摸到的都是视图.视图是用户界面的重要组成元素.例如,想要让用户实现文本输入时 ...

  7. Xamarin iOS教程之申请付费开发者账号下载证书

    Xamarin iOS教程之申请付费开发者账号下载证书 Xamarin iOS使用真机测试应用程序 在讲解iOS Simulator时,已经提到了虽然iOS Simulator可以模仿真实的设备,但是 ...

  8. Xamarin iOS教程之编辑界面编写代码

    Xamarin iOS教程之编辑界面编写代码 Xamarin iOS的Interface Builder Interface Builder被称为编辑界面.它是一个虚拟的图形化设计工具,用来为iOS应 ...

  9. Xamarin iOS教程之自定义视图

    Xamarin iOS教程之自定义视图 Xamarin iOS自定义视图 工具栏中的视图在实际应用开发中用的很多,但是为了吸引用户的眼球,开发者可以做出一些自定义的视图. [示例2-33]以下将实现一 ...

随机推荐

  1. Excel的单元格设置下拉选项并填充颜色

    如何在Excel的单元格中加入下拉选项   方法/步骤     第一步:打开excel文档,选中需加入下拉选项的单元格.      第二步:点击菜单中的“数据”->“数据有效性”->“数据 ...

  2. Spring.Net+NHibernate+Castle学习网站

    1.刘冬  http://www.cnblogs.com/GoodHelper/archive/2009/10/16/1584243.html 2.学习资料 http://www.cnblogs.co ...

  3. LeetCode:括号的分数【856】

    LeetCode:括号的分数[856] 题目描述 给定一个平衡括号字符串 S,按下述规则计算该字符串的分数: () 得 1 分. AB 得 A + B 分,其中 A 和 B 是平衡括号字符串. (A) ...

  4. QQ空间动态内容,好友信息,点赞爬虫脚本

    一.安装基础的软件包: 1.准备好火狐浏览器,并下载geckodriver,将geckodriver加入到环境变量:下载geckodriver的地址:https://pan.baidu.com/s/1 ...

  5. C#中ReferenceEquals和Equals的区别

    ReferenceEquals()判断两个字符串是否指向相同的内存地址:(判断引用) Equals,先判断两个字符串有相同的内存位置,是则两个字符串相等:否则逐字符比较两个字符串,判断是否相等(先判断 ...

  6. LAMP脚本

    A goal is a dream with a deadline. Much effort, much prosperity. 环境:CentOS release 6.5  2.6.32-431.e ...

  7. Sqoop-将MySQL数据导入到hive orc表

    sqoop创建并导入数据到hive orc表 sqoop import \ --connect jdbc:mysql://localhost:3306/spider \ --username root ...

  8. C# 实现汉字转拼音

    /// <summary> /// 生成拼音简码 /// </summary> /// <param name="unicodeString"> ...

  9. LinkedBlockingQueue 与ConcurrentLinkedQueue队列的不同与同

    LinkedBlockingQueue 的API中,从队列中获取元素,有以下几个方法: 1.take():原文:Retrieves and removes the head of this queue ...

  10. 使用ssm整合是创建Maven项目报错Failure to transfer com.thoughtworks.xstream:xstream:pom:1.3.1

    Description Resource Path Location TypeFailure to transfer com.thoughtworks.xstream:xstream:pom:1.3. ...