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
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
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
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
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
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
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
// 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
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
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
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
class CallViewController: UIViewController {
var call: SinchCall?
...
MainViewController.swift
@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
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
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
@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.