Handling incoming calls

In the previous section we added the capability of initiating the call. This section describes how to get notified of and handle incoming calls.

If you haven't yet initiated a call, go back and follow the steps in that section.

Sinch SDK requires APNs VoIP notifications to establish calls. Make sure you've uploaded your APNs signing keys to your Sinch application (see Create app section).

Starting from iOS13, incoming VoIP notifications have to be reported to CallKit or your app will be killed (refer to Sinch public docs for further details). For clarity, this guide will first describe the procedure to report calls to CallKit, while handling of incoming VoIP notification will be showed afterwards.

Report an incoming call to CallKit

To enable push notification usage in Sinch client, instantiate a SinchManagedPush object as an AppDelegate property, and request a device token for VoIP notifications:

AppDelegate.swift

Copy
Copied
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  func application(_ application: UIApplication, 
                   didFinishLaunchingWithOptions launchOptions: 
                       [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    sinchPush = SinchRTC.managedPush(
         forAPSEnvironment: SinchRTC.APSEnvironment.development)
    sinchPush.delegate = self
    sinchPush.setDesiredPushType(SinchManagedPush.TypeVoIP)
    ...
    return true
  }

Let's now extend AppDelegate to conform with SinchManagedPushDelegate, to handle incoming VoIP notifications. The implementation of SinchClientMediator.reportIncomingCall(withPushPayload:withCompletion:) will follow. Don't forget to forward the incoming push payload to Sinch client with SinchClient.relayPushNotification(withUserInfo:), which allows Sinch client to instantiate a new SinchCall object based on information contained in the push payload.

AppDelegate.swift

Copy
Copied
extension AppDelegate: SinchManagedPushDelegate {
  func managedPush(_ managedPush: SinchManagedPush,
                   didReceiveIncomingPushWithPayload payload: [AnyHashable: Any], 
                   for type: String) {
    os_log("didReceiveIncomingPushWithPayload: %{public}@", 
           log: self.customLog, 
           payload.description)
    // Request SinchClientProvider to report new call to CallKit
    sinchClientMediator.reportIncomingCall(withPushPayload: payload, 
          withCompletion: { err in DispatchQueue.main.async {
            self.sinchClientMediator.sinchClient?.relayPushNotification(
               withUserInfo: payload)
      }
      if err != nil {
        os_log("Error when reporting call to CallKit: %{public}@",
            log: self.customLog, type: .error, err!.localizedDescription)
      }
    })
  }
}

SinchClientMediator.swift

Copy
Copied
func reportIncomingCall(withPushPayload payload: [AnyHashable: Any], 
      withCompletion completion: @escaping (Error?) -> Void) {
  func reportIncomingCall(withPushPayload payload: [AnyHashable: Any], 
         withCompletion completion: @escaping (Error?) -> Void) {
    // Extract call information from the push payload
    let notification = queryPushNotificationPayload(payload)
    if notification.isCall && notification.isValid {
      let callNotification = notification.callResult

      guard callRegistry.callKitUUID(
                  forSinchId: callNotification.callId) == nil else { 
        return 
      }
      let cxCallId = UUID()
      callRegistry.map(callKitId: cxCallId, 
                       toSinchCallId: callNotification.callId)

      os_log("reportNewIncomingCallToCallKit: ckid:%{public}@ callId:%{public}@",
          log: customLog, cxCallId.description, callNotification.callId)
      // Request SinchClientProvider to report new call to CallKit
      let update = CXCallUpdate()
      update.remoteHandle = CXHandle(type: .generic, 
                                     value: callNotification.remoteUserId)
      update.hasVideo = callNotification.isVideoOffered

      // Reporting the call to CallKit
      provider.reportNewIncomingCall(with: cxCallId, update: update) { 
          (error: Error?) in
        if error != nil {
          // If we get an error here from the OS, 
          // it is possibly the callee's phone has
          // "Do Not Disturb" turned on; 
          // check CXErrorCodeIncomingCallError in CXError.h
          self.hangupCallOnError(withId: callNotification.callId)
        }
        completion(error)
      }
    }
  }
}

private func hangupCallOnError(withId callId: String) {
  guard let call = callRegistry.sinchCall(forCallId: callId) else {
    os_log("Unable to find sinch call for callId: %{public}@", log: customLog, 
           type: .error, callId)
    return
  }
  call.hangup()
  callRegistry.removeSinchCall(withId: callId)
}

Note that in order to properly react to the user tapping "Answer" button on CallKit UI, we must implement one more CXProviderDelegate method:

SinchClientMediator+CXProviderDelegate.swift

Copy
Copied
  func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
  guard sinchClient != nil else {
    os_log("SinchClient not assigned when CXAnswerCallAction. Failing action", 
           log: customLog, 
           type: .error)
    action.fail()
    return
  }
  // Fetch SinchCall from callRegistry
  guard let call = callRegistry.sinchCall(forCallKitUUID: action.callUUID) else {
    action.fail()
    return
  }

  os_log("provider perform action: CXAnswerCallAction: %{public}@", 
         log: customLog, 
         call.callId)
  sinchClient!.audioController.configureAudioSessionForCallKitCall()
  call.answer()
  action.fulfill()
}

Outgoing call UI

To react to the creation of a SinchCall object, after receiving a VoIP notification, SinchClientMediator has to act as a delegate of SinchCallClient.

SinchClientMediator.swift

Copy
Copied
func create(withUserId userId:String, 
            andCallback callback:@escaping (_ error: Error?) -> Void) {
  ...
  sinchClient?.callClient.delegate = self
  sinchClient?.start()
}

So SinchClientMediator has to react to incoming call by:

  • assigning the call delegate
  • adding the incoming Sinch call to CallRegistry , so that the call can be fetched during interaction with CallKit UI
  • propagating the event to the ViewControllers which handle the presentation layer, via SinchClientMediator delegate methods. In this implementation, AppDelegate acts as a delegate, and is passed as a parameter via constructor.

SinchClientMediator+SinchCallClientDelegate.swift

Copy
Copied
extension SinchClientMediator: SinchCallClientDelegate {
  func client(_ client: SinchRTC.SinchCallClient, 
              didReceiveIncomingCall call: SinchRTC.SinchCall) {
    os_log(
        "didReceiveIncomingCall with callId: %{public}@, from:%{public}@",
        log: customLog, call.callId, call.remoteUserId)
    os_log("app state:%{public}d",
           UIApplication.shared.applicationState.rawValue)
    call.delegate = self
    // We save the call object so we can either accept or deny it later when 
    // user interacts with CallKit UI.
    callRegistry.addSinchCall(call)

    if UIApplication.shared.applicationState != .background {
      delegate.handleIncomingCall(call)
    }
  }
}

Assign the delegate via constructor parameter:

SinchClientMediator.swift

Copy
Copied
// New SinchClientMediatorDelegate
protocol SinchClientMediatorDelegate: AnyObject {
  func handleIncomingCall(_ call: SinchCall)
}

// Assigning the delegate via constructor parameter
class SinchClientMediator : NSObject {
  ...
  weak var delegate: SinchClientMediatorDelegate!
  ...

  init(delegate: SinchClientMediatorDelegate) {
    ...
    self.delegate = delegate
    ... 
  }

In delegate method, navigate to CallViewController:

AppDelegate.swift

Copy
Copied
extension AppDelegate: SinchClientMediatorDelegate {
  func handleIncomingCall(_ call: SinchCall) {
    transitionToCallView(call)
  }

  private func transitionToCallView(_ call: SinchCall) {
    // Find MainViewController and present CallViewController from it.
    let sceneDelegate = UIApplication.shared.connectedScenes
        .first!.delegate as! SceneDelegate
    var top = sceneDelegate.window!.rootViewController!
    while top.presentedViewController != nil {
      top = top.presentedViewController!
    }
    let sBoard = UIStoryboard(name: "Main", bundle: nil)
    guard let callVC = sBoard.instantiateViewController(
          withIdentifier: "CallViewController") as? CallViewController else {
      preconditionFailure("Error CallViewController is expected")
    }
    top.present(callVC, animated: true)
  }
}

Ending a call

Now that it's finally possible to place and receive calls between two clients, we must provide a way to terminate the call. Add SinchClientMediator.end(call:) method which requests CallKit the termination of the ongoing call:

SinchClientMediator.swift

Copy
Copied
func end(call: SinchCall) {
  guard let uuid = callRegistry.callKitUUID(forSinchId: call.callId) else {
    return
  }

  let endCallAction = CXEndCallAction(call: uuid)
  let transaction = CXTransaction()
  transaction.addAction(endCallAction)

  callController.request(transaction, completion: { error in
    if let err = error {
      os_log("Error requesting end call transaction: %{public}@", 
             log: self.customLog, type: .error, err.localizedDescription)
    }
    self.callStartedCallback = nil
  })
}

And then implement the actual hangup action in corresponding CXProviderDelegate callback:

SinchClientMediator+CXProviderDelegate.swift

Copy
Copied
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
  guard client != nil else {
    os_log("SinchClient not assigned when CXEndCallAction. Failing action", 
           log: customLog, type: .error)
    action.fail()
    return
  }

  guard let call = callRegistry.sinchCall(forCallKitUUID: action.callUUID) else {
    action.fail()
    return
  }

  os_log("provider perform action: CXEndCallAction: %{public}@", 
         log: customLog, call.callId)
  call.hangup()
  action.fulfill()
}

Using Connection Inspector View, implement SinchClientMediator.end(call:) as a reaction to tapping the "Hangup" button in CallViewController. Note that CallViewController has to access the SinchCall object corresponding to the ongoing call; add a SinchCall property to CallViewController, and make sure to set it in both paths that lead to CallViewController:

CallViewController.swift

Copy
Copied
class CallViewController: UIViewController {
  var call: SinchCall?
  ...

MainViewController.swift

Copy
Copied
@IBAction func CallButtonPressed(_ sender: UIButton) {
  sinchClientMediator.call(userId: recipientName.text!) { 
       (result: Result<SinchCall, Error>) in
    if case let .success(call) = result {
      // set the property in CallViewController
      callVC.call = call
      self.present(callVC, animated: true, completion: nil)
          ...

AppDelegate.swift

Copy
Copied
private func transitionToCallView(_ call: SinchCall) {
  ...
  callVC.call = call
  top.present(callVC, animated: true)
}
}

Logging out

A user can decide to log out, to stop receiving push notifications, or deallocate SinchClient for better memory efficiency.

Add a new method SinchClientMediator.logout(withCompletion:):

SinchClientMediator.swift

Copy
Copied
func logout(withCompletion completion: () -> Void) {
  defer {
    completion()
  }

  guard let client = sinchClient else { return }

  if client.isStarted {
    // Remove push registration from Sinch backend
    client.unregisterPushNotificationDeviceToken()
    client.terminateGracefully()
  }
  sinchClient = nil
}

and, using Connection Inspector View, add the following method as a reaction to the user tapping the "Logout" button in MainViewController:

MainViewController.swift

Copy
Copied
@IBAction func LogoutButtonPressed(_ sender: Any) {
  sinchClientMediator.logout(withCompletion: {
    dismiss(animated: true)
  })
}

Next steps

Now that you've built a simple app to make and receive calls, learn more about the iOS SDK.

We'd love to hear from you!
Rate this content:
Still have a question?
 
Ask the community.