版本记录
版本号 | 时间 |
---|---|
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框架基本使用的源码,感兴趣的给个赞或者关注~~~