Making an audio call

This guide shows you to how to make an audio call in your iOS app. We assume you've already set up your iOS app with the In-app Calling iOS SDK. If you haven't already, create an app first.

Note:

Starting from iOS13, both incoming and outgoing calls should be reported and handled via CallKit. For further reading about the intentions and usage of CallKit integration, refer to Push notifications documentation.

Report an outgoing call to CallKit

First, let's add a few properties to SinchClientMediator:

  • a CXCallController object and a CXProvider object, which will be used to request the creation of new CallKit calls
  • a property to store the call creation callback
  • a CallRegistry object, to map Sinch's callIds to CallKit's callId. For an example implementation of CallRegistry, please refer to CallRegistry.swift file in Sinch's Swift sample app, bundled together with Swift SDK.

SinchClientMediator.swift

Copy
Copied
class SinchClientMediator : NSObject {
  typealias CallStartedCallback = (Result<SinchCall, Error>) -> Void

  var callStartedCallback: CallStartedCallback!
  let callController: CXCallController! = CXCallController()
  let callRegistry = CallRegistry()
  var provider: CXProvider!

  override init() {
    super.init()
    // set up CXProvider
    let providerConfig = CXProviderConfiguration(localizedName: "my_app")
    providerConfig.supportsVideo = false
    providerConfig.ringtoneSound = "ringtone.wav"
    provider = CXProvider(configuration: providerConfig)
    provider.setDelegate(self, queue: nil)
  }

Next, add SinchClientMediator.call(userId:withCallback:) method, which requests the initiation of a new CallKit call.

Note:

At this point the Sinch client is still not involved.

SinchClientMediator.swift

Copy
Copied
func call(userId destination:String, 
          withCallback callback: @escaping CallStartedCallback) {
  let handle = CXHandle(type: .generic, value: destination)
  let initiateCallAction = CXStartCallAction(call: UUID(), handle: handle)
  initiateCallAction.isVideo = false
  let initOutgoingCall = CXTransaction(action: initiateCallAction)
  // store for later use
  callStartedCallback = callback

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

This way, CallKit events will be handled by SinchClientMediator by implementing the callbacks of CXProviderDelegate protocol; note that conformity to NSObject is required.

For the time being, we're interested in implementing only a subset of the CXProviderDelegate methods:

  • notification of AVAudioSession events to the SinchClient:

SinchClientMediator+CXProviderDelegate.swift

Copy
Copied
  func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
    guard sinchClient != nil else {
      return
    }
    sinchClient!.callClient.provider(provider: provider, 
                                     didActivateAudioSession: audioSession)
  }

  func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
    guard sinchClient != nil else {
      return
    }
    sinchClient!.callClient.provider(provider: provider, 
                                     didDeactivateAudioSession: audioSession)
  }
  • starting a Sinch call as CXStartCallAction callback is invoked, by accessing SinchCallClient subcomponent which is the entry point of calling functionalities. If the call started successfully, the callId is stored in CallRegistry and SinchClientMediator is assigned as delegate of the call.

SinchClientMediator+CXProviderDelegate.swift

Copy
Copied
 func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
   defer {
     callStartedCallback = nil
   }

   guard sinchClient != nil else {
     action.fail()
     callStartedCallback?(.failure(Errors.clientNotStarted(
           "SinchClient not assigned when CXStartCallAction.")))
     return
   }
   // actual start of Sinch call
   let callResult = sinchClient!.callClient.callUser(withId: action.handle.value)
   switch callResult {
   case let .success(call):
     callRegistry.addSinchCall(call)
     callRegistry.map(callKitId: action.callUUID, toSinchCallId: call.callId)
     // Assigning the delegate of the newly created SinchCall.
     call.delegate = self
     action.fulfill()

   case let .failure(error):
     os_log("Unable to make a call: %s", log: customLog, type: .error, 
            error.localizedDescription)
     action.fail()
   }
   callStartedCallback?(callResult)
 }

Outgoing call UI

In this app, SinchClientMediator is the only delegate of SinchCalls and takes care of CallKit integration, but it also forwards events to CallViewController which controls call-related UI events.

In this implementation this is accomplished by implementing an Observer pattern, where CallViewController is the observer of SinchClientMediator.

SinchClientMediator.swift

Copy
Copied
class SinchClientMediator : NSObject {
  var observers: [Observation] = []
  ...

  private func fanoutDelegateCall(_ callback: (
         _ observer: SinchClientMediatorObserver) -> Void) {
    // Remove dangling before calling
    observers.removeAll(where: { $0.observer == nil })
    observers.forEach { callback($0.observer!) }
  }

  class Observation {
    init(_ observer: SinchClientMediatorObserver) {
      self.observer = observer
    }

    weak var observer: SinchClientMediatorObserver?
  }

  func addObserver(_ observer: SinchClientMediatorObserver) {
    guard observers.firstIndex(where: { $0.observer === observer }) != nil else {
      observers.append(Observation(observer))
      return
    }
  }

  func removeObserver(_ observer: SinchClientMediatorObserver) {
    if let idx = observers.firstIndex(where: { $0.observer === observer }) {
      observers.remove(at: idx)
    }
  }

And now make sure that CallViewController is added as an observer as soon as the view is loaded, and extend it to conform to SinchClientMediatorObserver observer.

CallViewController.swift

Copy
Copied
class CallViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    sinchClientMediator = appDelegate.sinchClientMediator
    sinchClientMediator.addObserver(self)
  }
}
extension CallViewController: SinchClientMediatorObserver {

  func callDidProgress(_ call: SinchCall) {
    playSoundFile("ringback.wav")
  }

  func callDidEstablish(_ call: SinchCall) {
    sinchClientMediator.sinchClient?.audioController.stopPlayingSoundFile()
  }

  func callDidEnd(_ call: SinchCall) {
    dismiss(animated: true)
    sinchClientMediator.sinchClient?.audioController.stopPlayingSoundFile()
    sinchClientMediator.removeObserver(self)
  }
}

Finally, using the Connection Inspector view, trigger SinchClientDelegate.call(userId:withCallback:) as a reaction to pushing the "Call" button in MainViewController, and present CallViewController in case of success.

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 {
      let sBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
      guard let callVC = sBoard.instantiateViewController(
           withIdentifier: "CallViewController") as? CallViewController else {
        preconditionFailure("Error: CallViewController is expected")
      }
      self.present(callVC, animated: true, completion: nil)

    } else if case let .failure(error) = result {
      os_log("Call failed failed: %{public}@", log: self.customLog, 
             type: .error, error.localizedDescription)
    }
  }
}
Note:

At this point you still can't receive calls; to test your implementation up to this point, you can try to place a call to a non-existing user and verify that the call fails with an error message along the lines of: "Unable to connect call (destination user not found)".

Next steps

Now that you've made a call, you can set up your application to handle incoming calls.

Was this page helpful?