Skip to main content

IOS Client SDK

Enable Telnyx real-time communication services on IOS. The Telnyx iOS WebRTC Client SDK provides all the functionality you need to start making voice calls from an iPhone.

Project structure:

  • SDK project: sdk module, containing all Telnyx SDK components as well as tests.
  • Demo application: app module, containing a sample demo application utilizing the sdk module.

Project Setup:

  1. Clone the repository
  2. Open the cloned repository in Xcode and hit the build button to build both the sdk and sample app:
  3. Connect a device or start an emulated device and hit the run button

Usage

Cocoapods

Adding Telnyx to your iOS Client Application:

If your xcode project is not using cocoapods yet, you will need to configure it.

  1. Open your podfile and add the TelnyxRTC.
pod 'TelnyxRTC', '~> 0.1.0'
  1. Install your pods. You can add the flag --repo-update to ensure your cocoapods has the specs updated.
pod install --repo-update
  1. Open your .xcworkspace
  2. Import TelnyxRTC at the top level of your class:
import TelnyxRTC
  1. Disable BITCODE (The GoogleWebRTC dependency has BITCODE disabled): Go to the Build Settings tab of your app target, search for “bitcode” and set it to “NO”

Screen Shot 2021-05-07 at 17 46 08

  1. Enable VoIP and Audio background modes: Go to Signing & Capabilities tab, press the +Capability button and add those background modes:

Screen Shot 2021-05-07 at 17 46 54

  1. Go to your Info.plist file and add the “Privacy - Microphone Usage Description” key with a description that your app requires microphone access in order to make VoIP calls.

Screen Shot 2021-05-07 at 17 48 17

  1. You are all set!

Swift Package Manager

Xcode has a built-in support for Swift package manager. To add a package :

  1. Select Files > Add Packages
  2. On the Swift Package Manager Screen, Search for the https://github.com/team-telnyx/telnyx-webrtc-ios.git package.
  3. Select the main brach and click Add Package

Screen Shot 2021-05-07 at 17 48 17

NB: if Add Package gets stuck downloading binaries try File > Packages > Reset Package Caches or Run the command rm -rf ~/Library/Caches/org.swift.swiftpm/ in terminal

Read more in Apple documentation

Hint: Use either Cocoapods or Swift Package Manager for Individual Packages to avoid Duplicate binaries

Telnyx Client

The TelnyxClient connects your application to the Telnyx backend, enabling you to make outgoing calls and handle incoming calls.

The main steps to get the SDK working are: 1 - Initialize the client. 2 - Set the delegate to listen to SDK events. 3 - Connect the client: Login to the backend in order to receive and start calls. 4 - Place outbound calls. 5 - Incoming calls.

1 - Initialize the client:

To begin you will need to instantiate the Telnyx Client:

// Initialize the client
let telnyxClient = TxClient()

Note: After pasting the above content, Kindly check and remove any new line added

2 - Listen SDK updates:

To begin you will need to instantiate the Telnyx Client:

Then you will need to set the client delegate in order to get SDK events and you should implement the protocol wherever you want to receive the events:

Example of the delegate usage on a ViewController:

// Set the delegate
telnyxClient.delegate = self

Note: After pasting the above content, Kindly check and remove any new line added

extension ViewController: TxClientDelegate {

func onRemoteCallEnded(callId: UUID) {
// Call has been removed internally.
}

func onSocketConnected() {
// When the client has successfully connected to the Telnyx Backend.
}

func onSocketDisconnected() {
// When the client from the Telnyx backend
}

func onClientError(error: Error) {
// Something went wrong.
}

func onClientReady() {
// You can start receiving incoming calls or
// start making calls once the client is fully initialized.
}

func onSessionUpdated(sessionId: String) {
// This function will be executed when a sessionId is received.
}

func onIncomingCall(call: Call) {
// Someone is calling you.
}

func onPushCall(call: Call) {
// If you have configured Push Notifications and app is in background or the Telnyx Client is disconnected
// this delegate method will be called after the push notification is received.
// Update the current call with the incoming call
}

// You can update your UI from here based on the call states.
// Check that the callId is the same as your current call.
func onCallStateUpdated(callState: CallState, callId: UUID) {
// handle the new call state
switch (callState) {
case .CONNECTING:
break
case .RINGING:
break
case .NEW:
break
case .ACTIVE:
break
case .DONE:
break
case .HELD:
break
}
}
}

Note: After pasting the above content, Kindly check and remove any new line added

3 - Connect the client:

In this step you need to set the parameters to configure your connection. The main parameters are the login credentials used to register into the Telnyx backend.

You can connect using your SIP credentials (sip user and password) or by an access token. You can check how to create your credentials on the following link

Login with SIP credentials:

let txConfigUserAndPassword = TxConfig(sipUser:SIP_USER,
password:SIP_PASSWORD)
do {
// Connect and login
try telnyxClient.connect(txConfig: txConfigUserAndPassword)
} catch let error {
print("Error \(error)")
}

Note: After pasting the above content, Kindly check and remove any new line added

Login with Access Token:

// Use a JWT Telnyx Token to authenticate
let txConfigToken = TxConfig(token: "MY_JWT_TELNYX_TOKEN")

do {
// Connect and login
// Use `txConfigToken`
try telnyxClient.connect(txConfig: txConfigToken)
} catch let error {
print("Error \(error)")
}

Note: After pasting the above content, Kindly check and remove any new line added

The TxConfig Structure:

Through the TxConfig structure you can configure the following optional parameters:

  • The Ringtone audio file.
  • The Ringback tone audio file.
  • The logging level.

4 - Outbound calls:

Once your client is fully connected and logged in you can place outbound calls from your iPhone. You can call to a phone number or to a SIP URI. To call a SIP URI pass the following as the destinationNumber parameter: [email protected]

Your client will be ready to place outbound calls only when is logged in. To determine whether your client is ready or not, you need to implement the TxClientDelegate and wait for the onClientReady() method to be called.

Example of usage:

 // Create a client instance
self.telnyxClient = TxClient()

// Assign the delegate to get SDK events
self.telnyxClient?.delegate = self

// Connect the client (Check TxClient class for more info)
self.telnyxClient?.connect(....)

// Create the call and start calling
self.currentCall = try self.telnyxClient?.newCall(callerName: "Caller name",
callerNumber: "155531234567",
// Destination is required and can be a phone number or SIP URI
destinationNumber: "18004377950",
callId: UUID.init(),
customHeaders:[String:String] = [:])

Note: After pasting the above content, Kindly check and remove any new line added

5 - Incoming calls:

In order to answer an incoming call you will need to be fully connected and logged in.

//Init your client
func initTelnyxClient() {
self.telnyxClient = TxClient()

// Assign the delegate to get SDK events
self.telnyxClient?.delegate = self

// Connect the client (Check TxClient class for more info)
self.telnyxClient?.connect(....)
}

extension ViewController: TxClientDelegate {
//....
func onIncomingCall(call: Call) {
//We are automatically answering any incoming call as an example, but
//maybe you want to store a reference of the call, and answer the call after a button press.
self.myCall = call.answer()
//We can retrieve custom headers passed from the caller side from our call object :
self.myCall.inviteCustomHeaders
}
}

Note: After pasting the above content, Kindly check and remove any new line added

Configure CallKit into your App:

PushKit requires you to use CallKit when handling VoIP calls. CallKit ensures that apps providing call-related services on a user’s device work seamlessly together on the user's device, and respect features like Do Not Disturb. CallKit also operates the system's call-related UIs, including the incoming or outgoing call screens. Use CallKit to present these interfaces and manage interactions with them.

For more information about CallKit you can check the official Apple docs .

General Setup:

  1. Import CallKit:
import CallKit
  1. Initialize CallKit
func initCallKit() {
let configuration = CXProviderConfiguration(localizedName: "TelnyxRTC")
configuration.maximumCallGroups = 1
configuration.maximumCallsPerCallGroup = 1
callKitProvider = CXProvider(configuration: configuration)
if let provider = callKitProvider {
provider.setDelegate(self, queue: nil)
}
}
  1. Implement CXProviderDelegate methods.

Audio Session Handling WebRTC + CallKit

To get CallKit properly working with the TelnyxRTC SDK you need to set the audio device state based on the CallKit AudioSession state like follows:

extension AppDelegate : CXProviderDelegate {

...

func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
self.telnyxClient?.isAudioDeviceEnabled = true
}

func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
self.telnyxClient?.isAudioDeviceEnabled = false
}
}

Reporting calls with CallKit

To properly report calls to callKit with right statuses, you need to invoke the following callKit methods at the right instances:

  1. Starting A New Call : When ever you start a call, report to callkit using the provider.reportCall() method.
        let callUpdate = CXCallUpdate()

callUpdate.remoteHandle = callHandle
callUpdate.supportsDTMF = true
callUpdate.supportsHolding = true
callUpdate.supportsGrouping = false
callUpdate.supportsUngrouping = false
callUpdate.hasVideo = false
provider.reportCall(with: uuid, updated: callUpdate)
  1. When user receives a Call : Use provider.reportNewIncomingCall(with: uuid, update: callUpdate) to report an incoming call. This sends a request to callKit the to provide the native call interface to the user.
        guard let provider = callKitProvider else {
print("AppDelegate:: CallKit provider not available")
return
}

let callHandle = CXHandle(type: .generic, value: from)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle

provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
// handle error
}
  1. When callee answers an outgoing call : Use provider.reportOutgoingCall(with: callKitUUID, connectedAt:nil) to report a connected outgoing call. This provides the time when the outgoing call goes to active to callKit.
        if let provider = self.callKitProvider,
let callKitUUID = self.callKitUUID {
let date = Date()
provider.reportOutgoingCall(with: callKitUUID, connectedAt:date)
}

NB : This should be used only when the call is outgoing.

Best Practices when Using PushNotifications with Callkit.

  1. When receiving calls from push notifications, it is always required to wait for the connection to the WebSocket before fulfilling the call answer action. This can be achieved by implementing the CXProviderDelegate in the following way (SDK version >=0.1.11):
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
self.telnyxClient?.answerFromCallkit(answerAction: action)
}

When the answerFromPush(answerAction: action) is called, Callkit sets the call state to connecting to alert the user that the call is being connected. Once the call is active, the timer starts.

Connecting StateActive call

The previous SDK versions requires handling the websocket connection state on the client side. It can be done in the following way:

var callAnswerPendingFromPush:Bool = false

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
print("AppDelegate:: ANSWER call action: callKitUUID [\(String(describing: self.callKitUUID))] action [\(action.callUUID)]")
if(currentCall != nil){
self.currentCall?.answer()
}else {
self.callAnswerPendingFromPush = true
}
action.fulfill()
}

func onPushCall(call: Call) {
print("AppDelegate:: TxClientDelegate onPushCall() \(call)")
self.currentCall = call //Update the current call with the incoming call

//Answer Call if call was answered from callkit
//This happens when there's a race condition between login and receiving PN
// when User answer's the call from PN and there's no Call or INVITE message yet. Set callAnswerPendingFromPush = true
// Whilst we wait fot onPushCall Method to be called
if(self.callAnswerPendingFromPush){
self.currentCall?.answer()
self.callAnswerPendingFromPush = false
}

}

Likewise for ending calls, the endCallFromCallkit(endAction:action) method should be called from :

func provider(_ provider: CXProvider, perform action: CXEndCallAction) {

self.telnyxClient?.endCallFromCallkit(endAction:action)

}

Calling this method solves the race condition, where call is ended before the client connects to the webserver. This way the call is ended on the callee side once a connection is established.

  1. Logs on the receiver's end are essential for thorough debugging of issues related to push notifications. However, the debugger is not attached when the app is completely killed. To address this, you can simply put the app in the background. VOIP push notifications should then come through, and the debugger should capture all logs.

Add Push Notifications capability to your Xcode project

  1. Open the xcode workspace associated with your app.
  2. In the Project Navigator (the left-hand menu), select the project icon that represents your mobile app.
  3. In the top-left corner of the right-hand pane in Xcode, select your app's target.
  4. Press the +Capabilities button.

Screen Shot 2021-11-26 at 13 34 12

  1. Enable Push Notifications

Screen Shot 2021-11-26 at 13 35 51

b. Configure PushKit into your app:

  1. Import pushkit
import PushKit
  1. Initialize PushKit:
private var pushRegistry = PKPushRegistry.init(queue: DispatchQueue.main)
...

func initPushKit() {
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = Set([.voIP])
}
  1. Implement PKPushRegistryDelegate
extension AppDelegate: PKPushRegistryDelegate {

// New push notification token assigned by APNS.
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
if (type == .voIP) {
// This push notification token has to be sent to Telnyx when connecting the Client.
let deviceToken = credentials.token.reduce("", {$0 + String(format: "%02X", $1) })
UserDefaults.standard.savePushToken(pushToken: deviceToken)
}
}

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
if (type == .voIP) {
// Delete incoming token in user defaults
let userDefaults = UserDefaults.init()
userDefaults.deletePushToken()
}
}

/**
This delegate method is available on iOS 11 and above.
*/
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
if (payload.type == .voIP) {
self.handleVoIPPushNotification(payload: payload)
}

if let version = Float(UIDevice.current.systemVersion), version >= 13.0 {
completion()
}
}

func handleVoIPPushNotification(payload: PKPushPayload) {
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] {

let callId = metadata["call_id"] as? String
let callerName = (metadata["caller_name"] as? String) ?? ""
let callerNumber = (metadata["caller_number"] as? String) ?? ""
let caller = callerName.isEmpty ? (callerNumber.isEmpty ? "Unknown" : callerNumber) : callerName


let uuid = UUID(uuidString: callId)

// Re-connect the client and process the push notification when is received.
// You will need to use the credentials of the same user that is receiving the call.
let txConfig = TxConfig(sipUser: sipUser,
password: password,
pushDeviceToken: "APNS_PUSH_TOKEN")


//Call processVoIPNotification method

try telnyxClient?.processVoIPNotification(txConfig: txConfig, serverConfiguration: serverConfig,pushMetaData: metadata)



// Report the incoming call to CallKit framework.
let callHandle = CXHandle(type: .generic, value: from)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
callUpdate.hasVideo = false

provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
if let error = error {
print("AppDelegate:: Failed to report incoming call: \(error.localizedDescription).")
} else {
print("AppDelegate:: Incoming call successfully reported.")
}
}
}
}
  1. If everything is correctly set-up when the app runs APNS should assign a Push Token.
  2. In order to receive VoIP push notifications. You will need to send your push token when connecting to the Telnyx Client.
 
let txConfig = TxConfig(sipUser: sipUser,
password: password,
pushDeviceToken: "DEVICE_APNS_TOKEN",
//You can choose the appropriate verbosity level of the SDK.
logLevel: .all)

// Or use a JWT Telnyx Token to authenticate
let txConfigToken = TxConfig(token: "MY_JWT_TELNYX_TOKEN",
pushDeviceToken: "DEVICE_APNS_TOKEN",
//You can choose the appropriate verbosity level of the SDK. Logs are disabled by default
logLevel: .all)

For more information about Pushkit you can check the official Apple docs .

Disable Push Notification

Push notfications can be disabled for the current user by calling :

telnyxClient.disablePushNotifications()

Note : Signing back in, using same credentials will re-enable push notifications.

Custum Headers

Passing Custom Headers

Custom headers can be passed to the telnyxClient?.newCall(..) and currentCall?.answer(..) method, as a dictionary with the key in the fromat X-[Name-of-Header] and a string value.

 let headers =  ["X-test1":"ios-header-test","X-test2":"ios-header-2"]

self.telnyxClient?.newCall(callerName: "Caller name",
callerNumber: "155531234567",
// Destination is required and can be a phone number or SIP URI
destinationNumber: "18004377950",
callId: UUID.init(),
customHeaders: headers)

// Passed for answerFromCallkit
telnyxClient?.answerFromCallkit(answerAction: action,customHeaders:headers)

// Passed for answer
currentCall?.answer(customHeaders: headers)

Accessing Custom Headers

Custom headers can be accessed for both incoming calls and outgoing calls using call.inviteCustomHeaders and call.answerCustomHeaders which return a disctionary.


// access inviteCustomHeaders for incoming call
func onIncomingCall(call: Call) {
print("AppDelegate:: TxClientDelegate onPushCall() \(call)")
self.currentCall = call //Update the current call with the incoming call
let headers = call.inviteCustomHeaders
}

// access inviteCustomHeaders for push notification call
func onPushCall(call: Call) {
print("AppDelegate:: TxClientDelegate onPushCall() \(call)")
self.currentCall = call //Update the current call with the incoming call
let headers = call.inviteCustomHeaders
}

//access headers for when callstate is updated
func onCallStateUpdated(callState: CallState, callId: UUID) {
if(callState == .ACTIVE){
// check if custom headers was passed for answered message
let headers = self.currentCall?.answerCustomHeaders
}
}

Before uploading your app to TestFlight:

The WebRTC SDK requires access to the device microphone in order to make audio calls. In order to avoid rejection from Apple when uploading a build to TestFlight, remember to add the following key into your info.plist:

  1. Privacy - Microphone Usage Description

Microphone permission will be requested when making the first outbound call or when receiving the first inbound call.

Questions? Comments? Building something rad? Join our Slack channel and share.

On this page