CallKit 以及 PushKit 简单使用(二)

628 阅读4分钟
通过CallKit 来模拟来电
  • 首先从官网例子demo把音频文件拿来使用当作来电铃声

截屏2022-11-15 13.55.03.png

  • 确保导入 CallKit 以及 PushKit框架

截屏2022-11-15 14.22.34.png

  • copy官网例子新建 SpeakerboxCall类
class SpeakerboxCall {
    
    let uuid:UUID
    let isOutgoing:Bool // 是否拨出
    var handle:String?
    
    var stateDidChange:(()->())?
    var hasStartedConnectingDidChange:(()->())?
    var hasConnectedDidChange:(()->())?
    var hasEndedDidChange:(()->())?
    
    var connectingDate: Date? {
        didSet {
            stateDidChange?()
            hasStartedConnectingDidChange?()
        }
    }

    var connectDate: Date? {
        didSet {
            stateDidChange?()
            hasConnectedDidChange?()
        }
    }

    var endDate: Date? {
        didSet {
            stateDidChange?()
            hasEndedDidChange?()
        }
    }

    var isOnHold = false {
        didSet {
            stateDidChange?()
        }
    }

    var isMuted = false {
        didSet {
            stateDidChange?()
        }
    }
    
    var hasStartedConnecting: Bool {
        get {
            return connectingDate != nil
        }
        set {
            connectingDate = newValue ? Date() : nil
        }
    }

    var hasConnected: Bool {
        get {
            return connectDate != nil
        }
        set {
            connectDate = newValue ? Date() : nil
        }
    }

    var hasEnded: Bool {
        get {
            return endDate != nil
        }
        set {
            endDate = newValue ? Date() : nil
        }
    }

    var duration: TimeInterval {
        guard let connectDate = connectDate else {
            return 0
        }

        return Date().timeIntervalSince(connectDate)
    }
    
    // MARK: - 初始化
    init(uuid: UUID, isOutgoing: Bool = false) {
        self.uuid = uuid
        self.isOutgoing = isOutgoing
    }
    
    // MARK: - Actions

    // 拨打电话
    func startSpeakerboxCall(completion: ((_ success: Bool) -> Void)?) {
        
        completion?(true)

        DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 3) {
            self.hasStartedConnecting = true

            DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
                self.hasConnected = true
            }
        }
    }

    // 来电
    func answerSpeakerboxCall() {
        hasConnected = true
    }
    
    // 介绍当前通话
    func endSpeakerboxCall() {
        hasEnded = true
    }
    
}
  • SpeakerboxCallManager
import CallKit

class SpeakerboxCallManager: NSObject {
    
    let callController = CXCallController()
    
    // MARK: - Actions

    // 开始拨打
    func startCall(handle: String, video: Bool = false) {
        let handle = CXHandle(type: .phoneNumber, value: handle)
        let startCallAction = CXStartCallAction(call: UUID(), handle: handle)

        startCallAction.isVideo = video

        let transaction = CXTransaction()
        transaction.addAction(startCallAction)

        requestTransaction(transaction)
    }

    // 介绍通话
    func end(call: SpeakerboxCall) {
        let endCallAction = CXEndCallAction(call: call.uuid)
        let transaction = CXTransaction()
        transaction.addAction(endCallAction)

        requestTransaction(transaction)
    }

    // 设置保持通话状态
    func setOnHoldStatus(for call: SpeakerboxCall, to onHold: Bool) {
        let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
        let transaction = CXTransaction()
        transaction.addAction(setHeldCallAction)

        requestTransaction(transaction)
    }

    // 开启拨打电话界面
    private func requestTransaction(_ transaction: CXTransaction) {
        callController.request(transaction) { error in
            if let error = error {
                print("Error requesting transaction:", error.localizedDescription)
            } else {
                print("Requested transaction successfully")
            }
        }
    }

    // MARK: - Call Management

    var calls = [SpeakerboxCall]()

//    @Published private(set) var calls = [SpeakerboxCall]()
    
    func callWithUUID(uuid: UUID) -> SpeakerboxCall? {
        guard let index = calls.firstIndex(where: { $0.uuid == uuid }) else { return nil }

        return calls[index]
    }

    /// Adds a call to the array of active calls.
    /// - Parameter call: The call  to add.
    func addCall(_ call: SpeakerboxCall) {
        calls.append(call)
    }

    /// Removes a call from the array of active calls if it exists.
    /// - Parameter call: The call to remove.
    func removeCall(_ call: SpeakerboxCall) {
        guard let index = calls.firstIndex(where: { $0 === call }) else { return }

        calls.remove(at: index)
    }

    /// Empties the array of active calls.
    func removeAllCalls() {
        calls.removeAll()
    }

}
  • CallProviderDelegate
import CallKit
import AVKit

class CallProviderDelegate: NSObject {
    
    let callManager: SpeakerboxCallManager
    private let provider: CXProvider
    
    // MARK: - 初始化
    init(callManager: SpeakerboxCallManager) {
        
        var providerConfiguration:CXProviderConfiguration
        if #available(iOS 14.0, *) {
            providerConfiguration = CXProviderConfiguration()
        } else {
            providerConfiguration = CXProviderConfiguration(localizedName: "CallPushPorject")
        }
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.supportsVideo = false
        providerConfiguration.supportedHandleTypes = [.phoneNumber]
        providerConfiguration.ringtoneSound = "Ringtone.aif"
        
        self.callManager = callManager
        provider = CXProvider(configuration: providerConfiguration)
        super.init()
        provider.setDelegate(self, queue: nil)
    }
    
    // MARK: - Handle Incoming Calls

    /// 向系统报告有来电
    func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) {
        
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
        update.hasVideo = hasVideo

        // 向系统报告有来电
        provider.reportNewIncomingCall(with: uuid, update: update) { error in
            /*
             Only add an incoming call to an app's list of calls if it's allowed, i.e., there is no error.
             Calls may be denied for various legitimate reasons. See CXErrorCodeIncomingCallError.
             */
            if error == nil {
                let call = SpeakerboxCall(uuid: uuid)
                call.handle = handle

                self.callManager.addCall(call)
            }

            completion?(error)
        }
    }

}

// MARK: - CXProviderDelegate
extension CallProviderDelegate: CXProviderDelegate {

    func providerDidReset(_ provider: CXProvider) {
        print("Provider did reset")

//        stopAudio()

        /*
         End any ongoing calls if the provider resets, and remove them from the app's list of calls
         because they are no longer valid.
         */
        for call in callManager.calls {
            call.endSpeakerboxCall()
        }

        // Remove all calls from the app's list of calls.
        callManager.removeAllCalls()
    }

    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
        // Create and configure an instance of SpeakerboxCall to represent the new outgoing call.
        let call = SpeakerboxCall(uuid: action.callUUID, isOutgoing: true)
        call.handle = action.handle.value

        /*
         Configure the audio session but do not start call audio here.
         Call audio should not be started until the audio session is activated by the system,
         after having its priority elevated.
         */
//        configureAudioSession()

        /*
         Set callbacks for significant events in the call's lifecycle,
         so that the CXProvider can be updated to reflect the updated state.
         */
        call.hasStartedConnectingDidChange = { [weak self] in
            self?.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate)
        }
        call.hasConnectedDidChange = {[weak self] in
            self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate)
        }

        // Trigger the call to be started via the underlying network service.
        call.startSpeakerboxCall { success in
            if success {
                // Signal to the system that the action was successfully performed.
                action.fulfill()

                // Add the new outgoing call to the app's list of calls.
                self.callManager.addCall(call)
            } else {
                // Signal to the system that the action was unable to be performed.
                action.fail()
            }
        }
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID.
        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
            action.fail()
            return
        }

        /*
         Configure the audio session but do not start call audio here.
         Call audio should not be started until the audio session is activated by the system,
         after having its priority elevated.
         */
//        configureAudioSession()

        // Trigger the call to be answered via the underlying network service.
        call.answerSpeakerboxCall()

        // Signal to the system that the action was successfully performed.
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID
        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
            action.fail()
            return
        }

        // Stop call audio when ending a call.
//        stopAudio()

        // Trigger the call to be ended via the underlying network service.
        call.endSpeakerboxCall()

        // Signal to the system that the action was successfully performed.
        action.fulfill()

        // Remove the ended call from the app's list of calls.
        callManager.removeCall(call)
    }

    func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
        // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID
        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
            action.fail()
            return
        }

        // Update the SpeakerboxCall's underlying hold state.
        call.isOnHold = action.isOnHold

        // Stop or start audio in response to holding or unholding the call.
        if call.isOnHold {
            
//            stopAudio()
        } else {
//            startAudio()
        }

        // Signal to the system that the action has been successfully performed.
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
        // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID
        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
            action.fail()
            return
        }

        call.isMuted = action.isMuted

        action.fulfill()
    }

    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
        print("Timed out", #function)

        // React to the action timeout if necessary, such as showing an error UI.
    }

    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        print("Received", #function)

        /*
         Start call audio media, now that the audio session is activated,
         after having its priority elevated.
         */
//        startAudio()
    }

    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        print("Received", #function)

        /*
         Restart any non-call related audio now that the app's audio session is deactivated,
         after having its priority restored to normal.
         */
    }
}
  • 在AppDelegate
let callManager = SpeakerboxCallManager()
var providerDelegate: CallProviderDelegate?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    self.registerPushNotifications()
    self.registerVoipPushNotifications()
    
    providerDelegate = CallProviderDelegate(callManager: callManager)
    
    return true
}
  • 收到Voip通知后显示来电页面
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    debugPrint("didReceiveIncomingPushWith")
    guard type == .voIP else {
        debugPrint("Callkit& pushRegistry didReceiveIncomingPush But Not VoIP")
        return
    }
    debugPrint("收到VoIP")
    

    let uuid = UUID()
    let handle = "影"

    displayIncomingCall(uuid: uuid, handle: handle, hasVideo: false)
}
// 显示来电
func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) {
    providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion)
}
  • 拨打电话
@objc func callAction() {
    debugPrint("打电话")
    let callsController = SpeakerboxCallManager()
    callsController.startCall(handle: "雷电将军",video: false)
}