Search code examples
iosswiftcallkit

call kit ui not loading when called from app delegate


I am unable to fire off the call kit UI incoming call from app delegate. How do I do this? I tried the speaker box example but it didn't help. When i run the reportIncomingCall method within ViewController it works. When i ru reportIncomingCall in AppDelegate it doesn't work. I need it to run in Appdelegate so i can send VoIP notification to report incoming call.

here is my app delegate:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate {

    class var shared: AppDelegate {
        return UIApplication.shared.delegate as! AppDelegate
    }

    var window: UIWindow?

    var providerDelegate: ViewController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        return true
    }

    func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) {

        //register for voip notifications
        let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
        voipRegistry.desiredPushTypes = Set([PKPushType.voIP])
        voipRegistry.delegate = self;
    }

    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenForType type: PKPushType) {

         NSLog("token invalidated")

    }

    func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, forType type: PKPushType) {

        //print out the VoIP token. We will use this to test the notification.
        NSLog("voip token: \(credentials.token)")

        print("didUpdatePushCredentials: %@ - Type: %@", credentials.token, type)
        var token: String = ""
        for i in 0..<credentials.token.count {
            token += String(format: "%02.2hhx", credentials.token[i] as CVarArg)
        }

        print(token)

    }

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) {

        print("notification receivd!")
        guard type == .voIP else { return }

        let uuidString = payload.dictionaryPayload["UUID"] as? String!
        let roomName = payload.dictionaryPayload["roomName"] as? String!
        print("uuid", uuidString!)
        print("roomName", roomName!)


        providerDelegate?.performStartCallAction(uuid: UUID(), roomName: "Test")

    }

    //Intents
    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
        guard let viewController = window?.rootViewController as? ViewController, let interaction = userActivity.interaction else {
            return false
        }

        var personHandle: INPersonHandle?

        if let startVideoCallIntent = interaction.intent as? INStartVideoCallIntent {
            personHandle = startVideoCallIntent.contacts?[0].personHandle
        } else if let startAudioCallIntent = interaction.intent as? INStartAudioCallIntent {
            personHandle = startAudioCallIntent.contacts?[0].personHandle
        }

        if let personHandle = personHandle {
            viewController.performStartCallAction(uuid: UUID(), roomName: personHandle.value)
        }

        return true
    }

}

and in the ViewController class:

extension ViewController : CXProviderDelegate {

    func providerDidReset(_ provider: CXProvider) {
        logMessage(messageText: "providerDidReset:")

        localMedia?.audioController.stopAudio()
    }

    func providerDidBegin(_ provider: CXProvider) {
        logMessage(messageText: "providerDidBegin")
        //_ = Timer.scheduledTimerWithTimeInterval(15, target: self, selector: #selector(ViewController.expireCall), userInfo: nil, repeats: false)
    }

    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        logMessage(messageText: "provider:didActivateAudioSession:")

        localMedia?.audioController.startAudio()
    }

    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        logMessage(messageText: "provider:didDeactivateAudioSession:")
    }

    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
        logMessage(messageText: "provider:timedOutPerformingAction:")
    }

    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
        logMessage(messageText: "provider:performStartCallAction:")

        localMedia?.audioController.configureAudioSession(.videoChatSpeaker)

        callKitProvider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)

        performRoomConnect(uuid: action.callUUID, roomName: action.handle.value)

        // Hang on to the action, as we will either fulfill it after we succesfully connect to the room, or fail
        // it if there is an error connecting.
        pendingAction = action
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        logMessage(messageText: "provider:performAnswerCallAction:")

        // NOTE: When using CallKit with VoIP pushes, the workaround from https://forums.developer.apple.com/message/169511
        //       suggests configuring audio in the completion block of the `reportNewIncomingCallWithUUID:update:completion:`
        //       method instead of in `provider:performAnswerCallAction:` per the Speakerbox example.
        // localMedia?.audioController.configureAudioSession()

        performRoomConnect(uuid: action.callUUID, roomName: self.roomTextField.text)

        // Hang on to the action, as we will either fulfill it after we succesfully connect to the room, or fail
        // it if there is an error connecting.
        pendingAction = action
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        NSLog("provider:performEndCallAction:")

        localMedia?.audioController.stopAudio()
        room?.disconnect()

        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
        NSLog("provier:performSetMutedCallAction:")
        toggleMic(sender: self)
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
        NSLog("provier:performSetHeldCallAction:")

        let cxObserver = callKitCallController.callObserver
        let calls = cxObserver.calls

        guard let call = calls.first(where:{$0.uuid == action.callUUID}) else {
            action.fail()
            return
        }

        if call.isOnHold {
            holdCall(onHold: false)
        } else {
            holdCall(onHold: true)
        }

        action.fulfill()
    }
}



extension ViewController {



     func performStartCallAction(uuid: UUID, roomName: String?) {
        let callHandle = CXHandle(type: .generic, value: roomName ?? "")
        let startCallAction = CXStartCallAction(call: uuid, handle: callHandle)
        let transaction = CXTransaction(action: startCallAction)

        callKitCallController.request(transaction)  { error in
            if let error = error {
                NSLog("StartCallAction transaction request failed: \(error.localizedDescription)")
                return
            }

            NSLog("StartCallAction transaction request successful")

            let callUpdate = CXCallUpdate()
            callUpdate.remoteHandle = callHandle
            callUpdate.supportsDTMF = false
            callUpdate.supportsHolding = true
            callUpdate.supportsGrouping = false
            callUpdate.supportsUngrouping = false
            callUpdate.hasVideo = true

            self.callKitProvider.reportCall(with: uuid, updated: callUpdate)
        }
    }

      func reportIncomingCall(uuid: UUID, roomName: String?, completion: ((NSError?) -> Void)? = nil) {
        let callHandle = CXHandle(type: .generic, value: roomName ?? "")

        print("calling!")
        let callUpdate = CXCallUpdate()
        callUpdate.remoteHandle = callHandle
        callUpdate.supportsDTMF = false
        callUpdate.supportsHolding = true
        callUpdate.supportsGrouping = false
        callUpdate.supportsUngrouping = false
        callUpdate.hasVideo = true

        callKitProvider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
            if error == nil {
                NSLog("Incoming call successfully reported.")

                // NOTE: CallKit with VoIP push workaround per https://forums.developer.apple.com/message/169511
                self.localMedia?.audioController.configureAudioSession(.videoChatSpeaker)
            } else {
                NSLog("Failed to report incoming call successfully: \(error?.localizedDescription).")
            }

            completion?(error as? NSError)
        }
    }

     func performEndCallAction(uuid: UUID) {
        let endCallAction = CXEndCallAction(call: uuid)
        let transaction = CXTransaction(action: endCallAction)

        callKitCallController.request(transaction) { error in
            if let error = error {
                NSLog("EndCallAction transaction request failed: \(error.localizedDescription).")
                return
            }

            NSLog("EndCallAction transaction request successful")
        }
    }

}

EDIT

As noted in the comments below, it's clear that I wasn't setting the delegate. I have the following init in the vc. When I try to set it in the didFinishLaunchingWithOptons function, it is requiring me to add in argument coder.

ViewController init
required init?(coder aDecoder: NSCoder) {
    let configuration = CXProviderConfiguration(localizedName: "TestApp")
    configuration.maximumCallGroups = 1
    configuration.maximumCallsPerCallGroup = 1
    configuration.supportsVideo = true
    if let callKitIcon = UIImage(named: "iconMask80") {
        configuration.iconTemplateImageData = UIImagePNGRepresentation(callKitIcon)
    }

    callKitProvider = CXProvider(configuration: configuration)
    callKitCallController = CXCallController()

    super.init(coder: aDecoder)

    callKitProvider.setDelegate(self, queue: nil)

}

Appdelegate/didFinishLoadingWithOptions

providerDelegate = ViewController(coder: NSCoder) //this is where its messing up.

Solution

  • Try this method if that helps you

    My Call Ui Class that will be used in AppDelegate

    class CallUI : NSObject {  
        static var shared = CallUI()
        var callControllerClass = CallUIController()
    
        func initCall(payloadResponse Response: [AnyHashable:Any]) {
            callControllerClass.getDataSortedFromPayload(PayloadResponse: Response)
        }
    }
    

    Class Used to handle call Kit Delegates

    class CallUIController : UIViewController, CXProviderDelegate {
    
        var payloadResponse : [AnyHashable:Any]?
        var notificationTypeRx : CallNotificationType?
        var providerName : String?
    
        let provider = CXProvider(configuration: CXProviderConfiguration(localizedName: SAppName))
        let update = CXCallUpdate()
        var uuidUsed : UUID?
        var providerConfiguration : CXProviderConfiguration?
    
        func getDataSortedFromPayload(PayloadResponse Response: [AnyHashable:Any]) {
            if let APSData = Response["aps"] as? [String:Any] {
                if let notifyType = APSData["type"] as? String {
                    if notifyType == "calling" {
                        self.notificationTypeRx = .StartCall
                        self.showCallUI(ProviderName: nameUsed ?? "")
                    }
                    else if notifyType == "disconnectCalling" {
                        /// Need to Disconnect Call
                        self.notificationTypeRx = .Endcall
                        /// Dismiss if any Loaded UI
                    }
                    else{
                        print("Type of notification wasn't found")
                    }
                }
            }
            else{
                print("Aps Data was not found")
            }
        }
    
        func showCallUI(ProviderName Name: String) {
    
            provider.setDelegate(self, queue: nil)
            uuidUsed = UUID()
            update.hasVideo = true
            update.remoteHandle = CXHandle(type: .phoneNumber, value: Name)
            provider.reportNewIncomingCall(with: uuidUsed!, update: update, completion: { error in })
        }
    
        func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
            /// Accept Action
        }
    
        func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
            /// Decline Action
        }
    
    
        func providerDidReset(_ provider: CXProvider) {
            print("Declined Status")
        }
    
    }
    

    AppDelegate Usage

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
       print("Payload API Response \(payload.dictionaryPayload)")
       if UIApplication.shared.applicationState == .background {
            CallUI.shared.initCall(payloadResponse: payload.dictionaryPayload)
       }
    }