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 aCXProvider
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 ofCallRegistry
, please refer toCallRegistry.swift
file in Sinch's Swift sample app, bundled together with Swift SDK.
SinchClientMediator.swift
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
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
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 accessingSinchCallClient
subcomponent which is the entry point of calling functionalities. If the call started successfully, the callId is stored inCallRegistry
andSinchClientMediator
is assigned as delegate of the call.
SinchClientMediator+CXProviderDelegate.swift
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 SinchCall
s 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
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
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
@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.