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

版本记录

版本号 时间
V1.0 2019.04.18 星期四

前言

苹果 iOS 10 新发布了一个新的框架CallKit,使第三方VOIP类型语音通话类APP有了更好的展现方式和用户体验的提升,接下来这几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. CallKit框架详细解析(一) —— 基本概览(一)
2. CallKit框架详细解析(二) —— 基本使用(一)
3. CallKit框架详细解析(三) —— 基本使用(二)

源码

1. Swift

首先看下工程组织结构

下面看一下sb中的内容

下面就是源码了

1. Audio.swift
import AVFoundation

func configureAudioSession() {
  print("Configuring audio session")
  let session = AVAudioSession.sharedInstance()
  do {
    try session.setCategory(.playAndRecord, mode: .voiceChat, options: [])
  } catch (let error) {
    print("Error while configuring audio session: \(error)")
  }
}

func startAudio() {
  print("Starting audio")
}

func stopAudio() {
  print("Stopping audio")
}
2. Call.swift
import Foundation

enum CallState {
  case connecting
  case active
  case held
  case ended
}

enum ConnectedState {
  case pending
  case complete
}

class Call {
  let uuid: UUID
  let outgoing: Bool
  let handle: String
  
  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(deadline: .now() + 3) {
      self.state = .connecting
      self.connectedState = .pending
      
      DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        self.state = .active
        self.connectedState = .complete
      }
    }
  }
  
  func answer() {
    state = .active
  }
  
  func end() {
    state = .ended
  }
}
3. CallManager.swift
import Foundation
import CallKit

class CallManager {
  var callsChangedHandler: (() -> Void)?
  private let callController = CXCallController()
  
  private(set) var calls: [Call] = []
  
  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 self = self else { return }
      self.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?()
  }
  
  func end(call: Call) {
    let endCallAction = CXEndCallAction(call: call.uuid)
    let transaction = CXTransaction(action: endCallAction)
    
    requestTransaction(transaction)
  }
  
  private func requestTransaction(_ transaction: CXTransaction) {
    callController.request(transaction) { error in
      if let error = error {
        print("Error requesting transaction: \(error)")
      } else {
        print("Requested transaction successfully")
      }
    }
  }
  
  func setHeld(call: Call, onHold: Bool) {
    let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
    
    let transaction = CXTransaction()
    transaction.addAction(setHeldCallAction)
    
    requestTransaction(transaction)
  }
  
  func startCall(handle: String, videoEnabled: Bool) {
    let handle = CXHandle(type: .phoneNumber, value: handle)
    
    let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
    startCallAction.isVideo = videoEnabled
    
    let transaction = CXTransaction(action: startCallAction)
    
    requestTransaction(transaction)
  }
}
4. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  let callManager = CallManager()
  var providerDelegate: ProviderDelegate!
  
  class var shared: AppDelegate {
    return UIApplication.shared.delegate as! AppDelegate
  }
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    providerDelegate = ProviderDelegate(callManager: callManager)
    return true
  }
  
  func displayIncomingCall(
    uuid: UUID,
    handle: String,
    hasVideo: Bool = false,
    completion: ((Error?) -> Void)?
  ) {
    providerDelegate.reportIncomingCall(
      uuid: uuid,
      handle: handle,
      hasVideo: hasVideo,
      completion: completion)
  }
}
5. ProviderDelegate.swift
import AVFoundation
import CallKit

class ProviderDelegate: NSObject {
  private let callManager: CallManager
  private let provider: CXProvider
  
  init(callManager: CallManager) {
    self.callManager = callManager
    provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
    
    super.init()

    provider.setDelegate(self, queue: nil)
  }
  
  static var providerConfiguration: CXProviderConfiguration = {
    let providerConfiguration = CXProviderConfiguration(localizedName: "Hotline")
    
    providerConfiguration.supportsVideo = true
    providerConfiguration.maximumCallsPerCallGroup = 1
    providerConfiguration.supportedHandleTypes = [.phoneNumber]
    
    return providerConfiguration
  }()
  
  func reportIncomingCall(
    uuid: UUID,
    handle: String,
    hasVideo: Bool = false,
    completion: ((Error?) -> Void)?
  ) {
    let update = CXCallUpdate()
    update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
    update.hasVideo = hasVideo
    
    provider.reportNewIncomingCall(with: uuid, update: update) { error in
      if error == nil {
        let call = Call(uuid: uuid, handle: handle)
        self.callManager.add(call: call)
      }
      
      completion?(error)
    }
  }
}

// MARK: - CXProviderDelegate
extension ProviderDelegate: CXProviderDelegate {
  func providerDidReset(_ provider: CXProvider) {
    stopAudio()
    
    for call in callManager.calls {
      call.end()
    }
    
    callManager.removeAllCalls()
  }
  
  func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
      action.fail()
      return
    }
    
    configureAudioSession()
    
    call.answer()

    action.fulfill()
  }
  
  func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
    startAudio()
  }
  
  func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
    guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
      action.fail()
      return
    }
    
    stopAudio()
    
    call.end()

    action.fulfill()

    callManager.remove(call: call)
  }
  
  func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
    guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
      action.fail()
      return
    }
    
    call.state = action.isOnHold ? .held : .active
    
    if call.state == .held {
      stopAudio()
    } else {
      startAudio()
    }
    
    action.fulfill()
  }
  
  func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
    let call = Call(uuid: action.callUUID, outgoing: true,
                    handle: action.handle.value)

    configureAudioSession()

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

    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()
      }
    }
  }
}
6. CallTableViewCell.swift
import UIKit

class CallTableViewCell: UITableViewCell {
  var callState: CallState? {
    didSet {
      guard let callState = callState else { return }
      
      switch callState {
      case .active:
        callStatusLabel.text = "Active"
      case .held:
        callStatusLabel.text = "On Hold"
      case .connecting:
        callStatusLabel.text = "Connecting..."
      default:
        callStatusLabel.text = "Dialing..."
      }
    }
  }
  
  var incoming: Bool = false {
    didSet {
      iconImageView.image = incoming ? #imageLiteral(resourceName: "incoming_arrow") : #imageLiteral(resourceName: "outgoing_arrow")
    }
  }
  
  var callerHandle: String? {
    didSet {
      callerHandleLabel.text = callerHandle
    }
  }

  @IBOutlet private var iconImageView: UIImageView!
  @IBOutlet private var callerHandleLabel: UILabel!
  @IBOutlet private var callStatusLabel: UILabel!
}
7. CallsViewController.swift
import UIKit

private let presentIncomingCallViewControllerSegue = "PresentIncomingCallViewController"
private let presentOutgoingCallViewControllerSegue = "PresentOutgoingCallViewController"
private let callCellIdentifier = "CallCell"

class CallsViewController: UITableViewController {
  var callManager: CallManager!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    callManager = AppDelegate.shared.callManager
    
    callManager.callsChangedHandler = { [weak self] in
      guard let self = self else { return }
      self.tableView.reloadData()
    }
  }
  
  @IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
    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)
    }
  }
}

// MARK: - UITableViewDataSource
extension CallsViewController {
  override func tableView(
    _ tableView: UITableView,
    numberOfRowsInSection section: Int
  ) -> Int {
    return callManager.calls.count
  }
  
  override func tableView(
    _ tableView: UITableView,
    cellForRowAt indexPath: IndexPath
  ) -> UITableViewCell {
    let call = callManager.calls[indexPath.row]
    
    let cell = tableView.dequeueReusableCell(withIdentifier: callCellIdentifier)
      as! CallTableViewCell
    cell.callerHandle = call.handle
    cell.callState = call.state
    cell.incoming = !call.outgoing
    
    return cell
  }
  
  override func tableView(
    _ tableView: UITableView,
    commit editingStyle: UITableViewCell.EditingStyle,
    forRowAt indexPath: IndexPath
  ) {
    let call = callManager.calls[indexPath.row]
    callManager.end(call: call)
  }
}

// MARK - UITableViewDelegate
extension CallsViewController {
  override func tableView(
    _ tableView: UITableView,
    titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath
  ) -> String? {
    return "End"
  }
  
  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()
  }
}
8. NewCallViewController.swift
import UIKit

class NewCallViewController: UIViewController {
  var handle: String? {
    return handleTextField.text
  }
  
  var incoming: Bool {
    return incomingSegmentedControl.selectedSegmentIndex == 0
  }
  
  var videoEnabled: Bool {
    return videoSwitch.isOn
  }
  
  @IBOutlet private var handleTextField: UITextField!
  @IBOutlet private var videoSwitch: UISwitch!
  @IBOutlet private var incomingSegmentedControl: UISegmentedControl!

  @IBAction private func cancel(_ sender: UIBarButtonItem) {
    dismiss(animated: true, completion: nil)
  }
}
9. CallDirectoryHandler.swift
import Foundation
import CallKit

class CallDirectoryHandler: CXCallDirectoryProvider {
  override func beginRequest(with context: CXCallDirectoryExtensionContext) {
    context.delegate = self
    
    // Check whether this is an "incremental" data request. If so, only provide the set of phone number blocking
    // and identification entries which have been added or removed since the last time this extension's data was loaded.
    // But the extension must still be prepared to provide the full set of data at any time, so add all blocking
    // and identification phone numbers if the request is not incremental.
    if context.isIncremental {
      addOrRemoveIncrementalBlockingPhoneNumbers(to: context)
      
      addOrRemoveIncrementalIdentificationPhoneNumbers(to: context)
    } else {
      addAllBlockingPhoneNumbers(to: context)
      
      addAllIdentificationPhoneNumbers(to: context)
    }
    
    context.completeRequest()
  }
  
  private func addAllBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1234 ]
    for phoneNumber in phoneNumbers {
      context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
    }
  }
  
  private func addOrRemoveIncrementalBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    // Retrieve any changes to the set of phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
    // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
    let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = [ 1_408_555_1234 ]
    for phoneNumber in phoneNumbersToAdd {
      context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
    }
    
    let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_800_555_5555 ]
    for phoneNumber in phoneNumbersToRemove {
      context.removeBlockingEntry(withPhoneNumber: phoneNumber)
    }
    
    // Record the most-recently loaded set of blocking entries in data store for the next incremental load...
  }
  
  private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1111 ]
    let labels = [ "RW Tutorial Team" ]
    
    for (phoneNumber, label) in zip(phoneNumbers, labels) {
      context.addIdentificationEntry(
        withNextSequentialPhoneNumber: phoneNumber,
        label: label
      )
    }
  }
  
  private func addOrRemoveIncrementalIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    // Retrieve any changes to the set of phone numbers to identify (and their identification labels) from data store. For optimal performance and memory usage when there are many phone numbers,
    // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
    let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = [ 1_408_555_5678 ]
    let labelsToAdd = [ "New local business" ]
    
    for (phoneNumber, label) in zip(phoneNumbersToAdd, labelsToAdd) {
      context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
    }
    
    let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_888_555_5555 ]
    
    for phoneNumber in phoneNumbersToRemove {
      context.removeIdentificationEntry(withPhoneNumber: phoneNumber)
    }
    
    // Record the most-recently loaded set of identification entries in data store for the next incremental load...
  }
}

// MARK: - CXCallDirectoryExtensionContextDelegate
extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
  func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
    // An error occurred while adding blocking or identification entries, check the NSError for details.
    // For Call Directory error codes, see the CXErrorCodeCallDirectoryManagerError enum in .
    //
    // This may be used to store the error details in a location accessible by the extension's containing app, so that the
    // app may be notified about errors which occured while loading data even if the request to load data was initiated by
    // the user in Settings instead of via the app itself.
  }
}

后记

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

你可能感兴趣的:(CallKit框架详细解析(四) —— 基本使用(三))