Make and receive an audio call with LiveCommunicationKit

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.

Please, check our reference application for full implementation and additional functionalities, which is available on GitHub.

Note:

LiveCommunicationKit is available starting from iOS 17.4. It was introduced by Apple as an alternative to CallKit in certain scenarios (for example, in regions like mainland China where CallKit cannot be used). It allows your app to integrate with the system’s calling interface (and even be set as the default calling app on the device) while you continue to use the Sinch SDK for call functionality.

Making an audio call

First, create a property in the SinchLiveCommunicationKitService class that will be used to work with LiveCommunicationKit: a ConversationManager instance to manage calls.
Copy
Copied
final class SinchLiveCommunicationKitService: NSObject {
  
  // An object that manages VoIP conversations and interacts
  // with the system's calling UI.
  private var conversationManager: ConversationManager!

  init(delegate: ConversationManagerDelegate) {
    super.init()

    let configuration = ConversationManager.Configuration(
      ringtoneName: Ringtone.incoming,
      iconTemplateImageData: UIImage(named: "AppIcon")?.pngData(),
      maximumConversationGroups: 1,
      maximumConversationsPerConversationGroup: 1,
      includesConversationInRecents: false,
      supportsVideo: true,
      // Identification of user for each call; .generic means it's based on UUID.
      supportedHandleTypes: [.generic]
    )
    self.conversationManager = ConversationManager(configuration: configuration)
    self.conversationManager.delegate = delegate
  }
}
Note:
In the above configuration, we set includesConversationInRecents to false, meaning calls will not appear in the device’s recent calls log. You can set this to true if you want outgoing calls to be included in Recents.
Next, add a CallRegistry object in SinchClientMediator, to map Sinch’s call IDs to LiveCommunicationKit conversation UUIDs. For example implementation of CallRegistry, please refer to the CallRegistry.swift file in Sinch’s Swift sample app, bundled together with Swift SDK.

Also, set the SinchClientMediator as the delegate of the ConversationManager via SinchLiveCommunicationKitService so that it can process incoming conversation events.
Copy
Copied
final class SinchClientMediator: NSObject {

  // Maps Sinch's call Ids to LiveCommunicationKit conversation UUIDs.
  private let callRegistry = CallRegistry()

  private let liveCommunicationKitService: SinchLiveCommunicationKitService?

  init() {
    super.init()
    ...
    
    // Set SinchClientMediator as ConversationManagerDelegate.
    self.liveCommunicationKitService =
      SinchLiveCommunicationKitService(delegate: self)
  }
}

Next, add SinchLiveCommunicationKitService.call(userId:uuid:callback:) method, which requests the initiation of a new LiveCommunicationKit call (conversation) with the system UI.
Note:
At this point the SinchClient is still not involved.
Copy
Copied
final class SinchLiveCommunicationKitService: NSObject {
  ...

  func call(userId: String, uuid: UUID, with completion: @escaping (Error?) -> Void) {
    let handle = Handle(type: .generic, value: userId)
    let startConversationAction = StartConversationAction(conversationUUID: uuid, handles: [handle], isVideo: false)

    Task {
      do {
        try await self.conversationManager.perform([startConversationAction])
        completion(nil)
      } catch {
        completion(error)
      }
    }
  }
}

And in SinchClientMediator implement the call(destination:with:) method, which will be used to initiate a new LiveCommunicationKit call:
Copy
Copied
final class SinchClientMediator: NSObject {
  ...

  func call(destination userId: String, with callback: @escaping CallStartedCallback) {
    let uuid = UUID()
    callStartedCallback = callback

    let errorCompletion: (Error?) -> Void = { [weak self] error in
      guard let self = self, let error = error else { return }

      DispatchQueue.main.async {
        self.callStartedCallback(.failure(error))
        self.callStartedCallback = nil
      }
    }

    self.liveCommunicationKit?.call(userId: userId, uuid: uuid, with: errorCompletion)
  }
}
This way, LiveCommunicationKit events will be handled by SinchClientMediator by implementing the callbacks of the ConversationManagerDelegate protocol.

At the moment, we're interested in implementing only a subset of the ConversationManagerDelegate methods:
  • notification of AVAudioSession events to the SinchClient, to change audio session active state
Copy
Copied
extension SinchClientMediator: ConversationManagerDelegate {

  func conversationManager(_ manager: ConversationManager,
                           didActivate audioSession: AVAudioSession) {
    self.sinchClient?.callClient.didActivate(audioSession: audioSession)
  }

  func conversationManager(_ manager: ConversationManager,
                           didDeactivate audioSession: AVAudioSession) {
    self.sinchClient?.callClient.didDeactivate(audioSession: audioSession)
  }
}
  • After a call start request succeeds, the conversationManager(_:perform:) callback will be invoked. At that point, a SinchCall should be created via the SinchCallClient (the entry point for calling functionality). If the call starts successfully, the callId should be stored in the CallRegistry and SinchClientMediator set as the call’s delegate.
  • To end all ongoing calls if the conversation manager resets, implement the conversationManagerDidReset(_:) callback of ConversationManagerDelegate.
Copy
Copied
extension SinchClientMediator: ConversationManagerDelegate {
  ...

  func conversationManager(_ manager: ConversationManager,
                           perform action: ConversationAction) {
    switch action {
      case let start as StartConversationAction: return self.perform(action: start)
      // Join and End actions will be handled further in the guide.
      default:
        action.fulfill()
    }
  }

  private func perform(action: StartConversationAction) {
    defer { callStartedCallback = nil }

    guard let callClient = self.sinchClient?.callClient else {
      action.fail()
      callStartedCallback?(.failure(CallError.noClient))
      return
    }

    guard let type = self.callTypes[action.conversationUUID.uuidString] else {
      action.fail()
      callStartedCallback?(.failure(CallError.noCallType(action.conversationUUID.uuidString)))
      return
    }

    guard let recipientIdentifier = action.handles.first?.value, !recipientIdentifier.isEmpty else {
      action.fail()
      callStartedCallback?(.failure(CallError.noRecepientIdentifier))
      return
    }

    let callResult = callResult = callClient.callUser(withId: recipientIdentifier)

    switch callResult {
      case .success(let call):
        self.callRegistry.addSinchCall(call)
        self.callRegistry.map(uuid: action.conversationUUID, to: call.callId)

        // Assigning the delegate of the newly created SinchCall
        // to track call establishment, progress and ending.
        call.delegate = self

        action.fulfill()
      case .failure(let error):
        // Report that unable to start call.
        action.fail()
    }

    callStartedCallback?(callResult)
  }

  func conversationManagerDidReset(_ manager: ConversationManager) {
    // End any ongoing calls if the provider resets, and remove
    // them from the app's list of calls because they are no longer valid.
    self.callRegistry.activeSinchCalls.forEach { $0.hangup() }
    // Remove all calls from the app's list of calls.
    self.callRegistry.reset()
  }
}

Outgoing call UI

In this app, SinchClientMediator is set as the delegate for all SinchCall instances (to handle LiveCommunicationKit integration), but it also forwards call events to the AudioCallViewController which manages the call UI.

In this implementation it is accomplished by implementing an Observer pattern, where AudioCallViewController is the observer of SinchClientMediator. SinchClientMediator should conform to SinchCallDelegate to listen to call event action and pass it to other observers.
Copy
Copied
protocol SinchClientMediatorObserver: SinchCallDelegate {}

final class SinchClientMediator: NSObject {
  ...
    
  // List of observers who listens for call actions through the whole application.
  private var observers: [SinchClientMediatorObserver?] = []
}

extension SinchClientMediator: SinchCallDelegate {
    
  // For each observer callback action will be called.
  private func fanoutDelegateCall(_ callback:
    (_ observer: SinchClientMediatorObserver?) -> Void) {
    observers.removeAll(where: { $0 === nil })
    observers.forEach { callback($0) }
  }

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

    observers.append(observer)
  }

  func removeObserver(_ observer: SinchClientMediatorObserver) {
    guard let index = observers.firstIndex(where: { $0 === observer }) else { return }

    observers.remove(at: index)
  }
}

Add methods for reporting outgoing calls and call end to LiveCommunicationKit in SinchLiveCommunicationKitServiceService:
Copy
Copied
final class SinchLiveCommunicationKitServiceService: NSObject {
  ...

  func reportOutgoingCallProgressed(uuid: UUID, time: Date?) {
    guard let time = time else { return }

    guard let conversation = self.conversationManager.conversations
      .first(where: { $0.uuid == uuid }) else { return }

    self.conversationManager.reportConversationEvent(.conversationStartedConnecting(time),
                                                     for: conversation)
  }

  func reportOutgoingCallAnswered(uuid: UUID, time: Date?) {
    guard let time = time else { return }

    guard let conversation = self.conversationManager.conversations
      .first(where: { $0.uuid == uuid }) else { return }

    self.conversationManager.reportConversationEvent(.conversationConnected(time),
                                                     for: conversation)
  }

  func reportCallEnd(uuid: UUID, time: Date?, endCause: EndCause) {
    guard let time = time else { return }

    guard let conversation = self.conversationManager.conversations
      .first(where: { $0.uuid == uuid }) else { return }

    self.conversationManager
      .reportConversationEvent(.conversationEnded(time, endCause.conversationEndReason),
                                                  for: conversation)
  }
}
Implement SinchCallDelegate methods to handle call progress, ringing, answer, establishment and ending. And notify observers whenever action occured:
  • callDidProgress(_ call:) to notify observers that call started and in progress
  • callDidRing(_ call:) to notify observers that call is ringing
  • callDidEstablish(_ call:) to notify observers that call was established
  • callDidAnswer(_ call:) to notify observers that call was answered
  • callDidEnd(_ call:) to notify observers that call has ended

As soon as outgoing call progressed, answered we need to report those events to LiveCommunicationKit. When call ended we report it to LiveCommunicationKit, as well.

Copy
Copied
// By implementing this we can handle call establishment, progress
// and ending wherever observers were added.
extension SinchClientMediator: SinchCallDelegate {
  ...
    
  func callDidProgress(_ call: SinchCall) {
    if let uuid = self.callRegistry.uuid(from: call.callId), call.direction == .outgoing {
      self.liveCommunicationKitService?
        .reportOutgoingCallProgressed(uuid: uuid, time: call.details.startedTime)
    }
    self.fanoutDelegateCall { $0?.callDidProgress(call) }
  }

  func callDidRing(_ call: SinchCall) {
    self.fanoutDelegateCall { $0?.callDidRing(call) }
  }

  func callDidAnswer(_ call: SinchRTC.SinchCall) {
    if let uuid = self.callRegistry.uuid(from: call.callId), call.direction == .outgoing {
      self.liveCommunicationKitService?
        .reportOutgoingCallAnswered(uuid: uuid, time: call.details.establishedTime)
    }
    self.fanoutDelegateCall { $0?.callDidAnswer(call) }
  }

  func callDidEstablish(_ call: SinchCall) {
    self.fanoutDelegateCall { $0?.callDidEstablish(call) }
  }

  func callDidEnd(_ call: SinchCall) {
    defer { call.delegate = nil }

    if let uuid = self.callRegistry.uuid(from: call.callId) {
      self.liveCommunicationKitService?.reportCallEnd(uuid: uuid,
                                                      time: call.details.endedTime,
                                                      endCause: call.details.endCause)
    }

    self.callRegistry.removeSinchCall(withId: call.callId)

    if call.details.endCause == .error {
      // Report call ended with error
    } else {
      // Report call ended with success
    }
    self.fanoutDelegateCall { $0?.callDidEnd(call) }
  }
}

Next, be sure to add AudioCallViewController as an observer when its view loads, and extend that class to conform to the SinchClientMediatorObserver protocol.
Copy
Copied
final class AudioCallViewController: UIViewController {

  var sinchClientMediator: SinchClientMediator?
  // AudioCallViewController holds the call object to be able to end the call.
  var call: SinchCall?

  override func viewDidLoad() {
    super.viewDidLoad()

    // Add observer to track call actions.
    sinchClientMediator?.addObserver(self)
  }
}

extension AudioCallViewController: SinchClientMediatorObserver {

  func callDidProgress(_ call: SinchCall) {
    self.callInfoLabel.text = "Initiating..."
  }

  func callDidRing(_ call: SinchCall) {
    self.callInfoLabel.text = "Ringing..."

    let audio = Ringtone.ringback
    let path = Bundle.main.path(forResource: audio, ofType: nil)

    do {
      try sinchClientMediator?.sinchClient?
        .audioController
        .startPlayingSoundFile(withPath: path, looping: true)
    } catch {
      // Report error if sound file was not played.
    }
  }

  func callDidAnswer(_ call: SinchRTC.SinchCall) {
    self.callInfoLabel.text = "Connecting..."

    // Stop playing sound when call was answered.
    sinchClientMediator?.sinchClient?
      .audioController
      .stopPlayingSoundFile()
  }

  func callDidEstablish(_ call: SinchCall) {
    // UI setup
    guard timer == nil else { return }

    timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
      let establishedTime = call.details.establishedTime ?? Date()
      let interval = Int(Date().timeIntervalSince(establishedTime))

      let minutes = Int(interval / 60).timePresentation
      let seconds = Int(interval % 60).timePresentation

      // Displays some call information when call was established.
      self.duration = "\(minutes):\(seconds)"
      self.callInfoLabel.text = "\(self.duration) with \(call.remoteUserId)"
    })
  }

  func callDidEnd(_ call: SinchCall) {
    timer?.invalidate()
    timer = nil

    // Finish call, by stop playing sound, dimissing and removing observers.
    dismiss(animated: true)
    sinchClientMediator?.sinchClient?
      .audioController
      .stopPlayingSoundFile()
    sinchClientMediator?.removeObserver(self)

    dismiss(animated: true)
  }
}

Finally, using the Connection Inspector view, trigger sinchClientMediator?.call(destination:with:) as a reaction to pushing the "Call" button in MainViewController, and present AudioCallViewController in case of success.
Copy
Copied
final class MainViewController: UIViewController {
  ...

  var sinchClientMediator: SinchClientMediator?

  @IBAction private func call(_ sender: Any) {
    let recipient = recipientNameTextField.text ?? ""

    sinchClientMediator?.call(destination: recipient,
                              type: callType) {
                                [weak self] (result: Result<SinchCall, Error>) in
      guard let self = self else { return }

      switch result {
        // On success transfers to call view controller.
        case .success(let call):
          let audioCallViewController: AudioCallViewController =
              self.prepareViewController(identifier: "call")

          // Pass call to be able to finish it.
          audioCallViewController.call = call

          self.present(audioCallViewController, animated: true)
        case .failure(let error):
          // Report error if call was not initiated.
      }
    }
  }
}
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)".

Receiving an audio call

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).

As mentioned above, incoming VoIP notifications must be reported to the system call interface (LiveCommunicationKit) or your app will be killed by the system if they are not handled (refer to Sinch public docs. This section describes how to get notified of and handle incoming calls, describe how to report calls to LiveCommunicationKit, handling and showing incoming VoIP notification.

Report an incoming call to LiveCommunicationKit

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

  // Create instance to enable push notification.
  private var sinchPush: SinchManagedPush?

  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions:
                      [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    sinchPush = SinchRTC.managedPush(forAPSEnvironment: .development)
    sinchPush?.delegate = self
    sinchPush?.setDesiredPushType(SinchManagedPush.TypeVoIP)

    return true
  }
}

Let's now extend AppDelegate to conform to SinchManagedPushDelegate and handle incoming VoIP notifications. The implementation of SinchClientMediator.reportIncomingCall(withPushPayload:withCompletion:) will follow.

Don't forget to forward the incoming push payload to SinchClient with SinchClient.relayPushNotification(withUserInfo:), which allows Sinch client to instantiate a new SinchCall object based on information contained in the push payload.
Copy
Copied
// Conform to SinchManagedPushDelegate to handle VoIP notification.
extension AppDelegate: SinchManagedPushDelegate {
    
  func managedPush(_ managedPush: SinchRTC.SinchManagedPush,
                   didReceiveIncomingPushWithPayload payload: [AnyHashable : Any],
                   for type: String) {
    sinchClientMediator.reportIncomingCall(with: payload, and: { error in
      DispatchQueue.main.async {
        // Forward the incoming push payload to Sinch client.
        sinchClientMediator.sinchClient?
            .relayPushNotification(withUserInfo: payload)
      }

      guard let error = error else { return }

      // Report error if push notification was not processed correctly.
    })
  }
}

Create a SinchLiveCommunicationKitService.reportIncomingCall(localUserId:remoteUserId:uuid:with:) to handle reporting the incoming call to LiveCommunicationKit. This method will be called from SinchClientMediator.reportIncomingCall(withPushPayload:and:) and will handle push notification and reporting of a new call.
Copy
Copied
final class SinchLiveCommunicationKitService: NSObject {
  ...

  func reportIncomingCall(localUserId: String,
                          remoteUserId: String,
                          uuid: UUID,
                          with completion: @escaping (Error?) -> Void) {
    let localHandle = Handle(type: .generic, value: localUserId)
    let remoteHandle = Handle(type: .generic, value: remoteUserId)

    let update = Conversation.Update(localMember: localHandle,
                                     activeRemoteMembers: [remoteHandle],
                                     capabilities: nil)

    Task {
      do {
        try await self.conversationManager
          .reportNewIncomingConversation(uuid: uuid, update: update)

        completion(nil)
      } catch {
        completion(error)
      }
    }
  }
}
Copy
Copied
final class SinchClientMediator: NSObject {
  ...

  func reportIncomingCall(with pushPayload: [AnyHashable: Any],
                        and completion: @escaping (Error?) -> Void) {
    // Extract call information from the push payload.
    let notification = queryPushNotificationPayload(pushPayload)

    guard notification.isCall, notification.isValid else { return }

    let callNotification = notification.callResult
    let callId = callNotification.callId

    guard self.callRegistry.uuid(from: callId) == nil else { return }

    let uuid = UUID()
    self.callRegistry.map(uuid: uuid, to: callId)

    self.liveCommunicationKit?
      .reportIncomingCall(localUserId: self.localUserId,
                          remoteUserId: callNotification.remoteUserId,
                          uuid: uuid,
                          with: { [weak self] error in
      guard let self = self else { return }

      // If we get an error here from the OS, it is
      // possibly the callee's phone has "Do Not Disturb" turned on
      self.hangupCall(with: callId, on: error)
      completion(error)
    })
  }

  // If error occured, just finish the call.
  private func hangupCall(with callId: String, on error: Error?) {
    guard let error = error else { return }

    guard let call = self.callRegistry.sinchCall(for: callId) else { return }

    call.hangup()
    self.callRegistry.removeSinchCall(withId: callId)
  }
}

Note that in order to properly handle the user tapping the “Answer” button in the system’s incoming call UI, we must implement performing of the JoinConversationAction for ConversationManagerDelegate:
Copy
Copied
extension SinchClientMediator: ConversationManagerDelegate {
  ...

  func conversationManager(_ manager: ConversationManager,
                           perform action: ConversationAction) {
    switch action {
      case let start as StartConversationAction: return self.perform(action: start)
      case let join as JoinConversationAction: return self.perform(action: join)
      // End action will be handled further in the guide.
      default:
        action.fulfill()
    }
  }

  private func perform(action: JoinConversationAction) {
    guard self.sinchClient != nil else {
      action.fail()
      return
    }

    guard let call = self.callRegistry.sinchCall(from: action.conversationUUID) else {
      action.fail()
      return
    }

    call.answer()
    action.fulfill()
  }
}

Handling incoming call

To react to the creation of a SinchCall after receiving a VoIP notification, SinchClientMediator should be set as the delegate of the SinchCallClient.
Copy
Copied
final class SinchClientMediator: NSObject {
  ...

  func createAndStart(with userId: String,
                      and callback: @escaping (_ error: Error?) -> Void) {
    ...

    sinchClient.callClient.delegate = self
    sinchClient.start()
  }
}
To react to an incoming call inside SinchClientMediator:
  • assign a call delegate, to handle call progress
  • add call to the CallRegistry to fetch it in LiveCommunicationKit callbacks
  • possibly propagate the incoming call event to UI controllers (in this example, AppDelegate handles it)
Copy
Copied
extension SinchClientMediator: SinchCallClientDelegate {

  func client(_ client: SinchRTC.SinchCallClient,
              didReceiveIncomingCall call: SinchRTC.SinchCall) {
    // To handle call events properly, it's important to set call delegate.
    call.delegate = self

    self.callRegistry.addSinchCall(call)

    guard UIApplication.shared.applicationState != .background else { return }

    delegate?.handleIncomingCall(call)
  }
}

Create a new protocol called SinchClientMediatorDelegate (responsible for handling incoming calls), and add a weak delegate property of that type to the SinchClientMediator class.
Copy
Copied
// Create SinchClientMediatorDelegate to handle incoming calls.
protocol SinchClientMediatorDelegate: AnyObject {

  func handleIncomingCall(_ call: SinchCall)
}

final class SinchClientMediator: NSObject {
  ...
    
  weak var delegate: SinchClientMediatorDelegate?
}

Assign AppDelegate as the SinchClientMediatorDelegate for your SinchClientMediator instance (to handle incoming call events).
Copy
Copied
class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions:
                      [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Assign delegate to handle incoming calls
    // throughout the application in AppDelegate.
    sinchClientMediator.delegate = self
  }
}

Implement the SinchClientMediatorDelegate method handleIncomingCall(_:). In this implementation, present an AudioCallViewController and pass the incoming SinchCall to it.
Copy
Copied
// Implementation to handle incoming call.
extension AppDelegate: SinchClientMediatorDelegate {
    
  // Navigate to call controller during incoming call.
  func handleIncomingCall(_ call: SinchCall) {
    transitionToCallViewController(for: call)
  }

  private func transitionToCallViewController(_ call: SinchCall) {
    guard let rootViewController = window?.rootViewController else { return }

    let presentedViewController =
        rootViewController.presentedViewController ?? rootViewController

    let presentingViewController =
        prepareAudioCallViewController(for: call,
                                       presentedViewController: presentedViewController)

    guard let presentingViewController = presentingViewController else { return }

    presentedViewController.present(presentingViewController, animated: true)
  }

  private func prepareAudioCallViewController(for call: SinchCall,
                                              presentedViewController: UIViewController)
                                                  -> AudioCallViewController? {
    let audioCallViewController = presentedViewController
        .prepareViewController(identifier: "call") as? AudioCallViewController

    // Pass call to be able to finish it or get information of SinchCall.
    audioCallViewController?.call = call

    return audioCallViewController
  }
}

Ending an audio call

Now that it’s possible to place and receive calls, we must provide a way to terminate the call.
Add SinchLiveCommunicationKitService.end(uuid:with:) and SinchClientMediator.end(call:), which will request LiveCommunicationKit to terminate an ongoing call.
Copy
Copied
final class SinchLiveCommunicationKitService: NSObject {
  ...

  func end(uuid: UUID, with completion: @escaping (Error?) -> Void) {
    let endConversationAction = EndConversationAction(conversationUUID: uuid)

    Task {
      do {
        // Request to end a call to LiveCommunicationKit.
        try await self.conversationManager.perform([endConversationAction])
        completion(nil)
      } catch {
        completion(error)
      }
    }
  }
}
Copy
Copied
final class SinchClientMediator: NSObject {
  ...

  func end(call: SinchCall) {
    guard let uuid = self.callRegistry.uuid(from: call.callId) else { return }

    let errorCompletion: (Error?) -> Void = { [weak self] error in
      guard let self = self else { return }

      if let error = error {
        // Handle error if call was not ended correctly.
      }

      self.callStartedCallback = nil
    }

    self.liveCommunicationKit?.end(uuid: uuid, with: errorCompletion)
  }
}

Then implement the actual hang-up action in the corresponding ConversationManagerDelegate callback.
Copy
Copied
extension SinchClientMediator: ConversationManagerDelegate {
  ...

  func conversationManager(_ manager: ConversationManager, perform action: ConversationAction) {
    switch action {
      case let start as StartConversationAction: return self.perform(action: start)
      case let join as JoinConversationAction: return self.perform(action: join)
      case let end as EndConversationAction: return self.perform(action: end)
      default:
        action.fulfill()
    }
  }

  private func perform(action: EndConversationAction) {
    guard self.sinchClient != nil else {
      action.fail()
      return
    }

    guard let call = self.callRegistry.sinchCall(from: action.conversationUUID) else {
      action.fail()
      return
    }

    call.hangup()
    action.fulfill()
  }
}

Using the Connections Inspector, hook up the ‘Hangup’ button in AudioCallViewController to call sinchClientMediator?.end(call:) when tapped.

Note that AudioCallViewController needs access to the SinchCall object for the ongoing call. Add a SinchCall property to AudioCallViewController and ensure it is set in both code paths that lead to this controller (MainViewController, AppDelegate).
Copy
Copied
final class AudioCallViewController: UIViewController {
  ...

  // To end call, should be passed to AudioCallViewController.
  var call: SinchCall?

  @IBAction private func hangup(_ sender: Any) {
    guard let call = call else { return }

    sinchClientMediator?.end(call: call)

    dismiss(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:).
Copy
Copied
final class SinchClientMediator: NSObject {
  ...

  func logout(with completion: () -> Void) {
    defer { completion() }

    guard let client = sinchClient else { return }

    // Termination of client.
    if client.isStarted {
      // Remove push registration from Sinch backend.
      client.unregisterPushNotificationDeviceToken()
      client.terminateGracefully()
    }

    sinchClient = nil
    liveCommunicationKitService = nil
  }
}

Using Connection Inspector View, invoke the following method as a reaction after tapping the "Logout" button in MainViewController.

Copy
Copied
final class MainViewController: UIViewController {
  ...

  // Connect logout action to button.
  @IBAction private func logout(_ sender: Any) {
    sinchClientMediator?.logout { [weak self] in
      guard let self = self else { return }

      self.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.