How to Make Phone Calls with the Nexmo Client SDK on iOS

In this guide, you'll learn how to place a phone call from a Nexmo application to a phone device (PSTN) by implementing a webhook and linking that to a Nexmo application.

You will create an app to place a call. The app will log in a user called Jane. After logging in, Jane is able to place a call as well as to end it.

Nexmo Concepts

Before proceeding any further, here are couple of concepts that you'll need to understand.

A Nexmo application allows you to easily use Nexmo products, in this case the Voice API to build voice applications in the Cloud.

A Nexmo application requires two URLs as parameters:

  • answer_url - Nexmo will make a request to this URL as soon as someone makes a call to your Nexmo number. It contains the actions that will happen throughout the call.
  • event_url - Nexmo sends event information asynchronously to this URL when the call status changes; this ultimately depicts the flow of the call.

Both URLs need to return JSON and follow the Nexmo Call Control Object (NCCO) reference. In the example below, you will define an NCCO that reads a predefined text for an incoming call, using the Text to Speech engine.

A Nexmo virtual number will be associated with the app and serve as the "entry point" to it - this is the number you'll call to test the application.

For more information on Nexmo applications please visit the Nexmo API Reference.)

Prerequisites

Application webhook

For your application to place a phone call, you'll need to provide a URL as the Answer URL webhook. For the purpose of this tutorial, you will create a gist with the content below:

[
    {
        "action": "talk",
        "text": "Please wait while we connect you."
    },
    {
        "action": "connect",
        "timeout": 20,
        "from": "YOUR_NEXMO_NUMBER",
        "endpoint": [
            {
                "type": "phone",
                "number": "CALLEE_PHONE_NUMBER"
            }
        ]
    }
]

Do not forget to replace YOUR_NEXMO_NUMBER and CALLEE_PHONE_NUMBER with the relevant values for your app.

Once created, add the gist raw URL (make sure you're using the raw version) to your Nexmo dashboard. To do this, navigate to applications, select your application and click the 'Edit' button. Set the application's Answer URL and click 'Save changes'.

You will need to repeat this process every time you're changing the gist as a new revision (with the new raw URL) is being created.

Note: The gist you created is specific to this tutorial. In a real-life scenario, the Answer URL should be provided by a purposely built web solution. Your backend should provide that can serve custom NCCOs and, for this case, receive and validate the phone number dialled from the app.

The Starter Project

Clone this Github project.

Using the Github project you cloned, in the Starter app, with XCode:

  1. Open Constants.swift file and replace the user token:

    enum Constant {
        static var userName = "Jane"
        static var userToken = "" //TODO: swap with a token for Jane
    }
    
  2. Open ViewController.swift file and make sure the following lines exist:

  • import NexmoClient - imports the sdk
  • var client: NXMClient? - property for the client instance
  • var call: NXMCall? - property for the call instance

Clone this Github project.

From the Github project you cloned, open the Starter app using XCode:

  1. Open IAVAppDefine.h file and replace the user id and token:

        #define kJaneUserId @"" //TODO: swap with Jane's user id
        #define kJaneToken @"" //TODO: swap with a token for Jane
    
  2. Open ViewController.m file and make sure the following lines exist:

  • #import <NexmoClient/NexmoClient.h> - imports the sdk
  • @property NXMClient *nexmoClient; - property for the client instance
  • @property NXMCall *ongoingCall; - property for the call instance

Login

Using the Nexmo Client SDK should start with logging in to NexmoClient, using a jwt user token.

In production apps, your server would authenticate the user, and would return a correctly configured JWT to your app.

For testing and getting started purposes, you can use the Nexmo CLI to generate JWTs.

Open ViewController.swift. Explore the setup methods that were written for you on viewDidLoad.

Now locate the following line //MARK: - Setup Nexmo Client and complete the setupNexmoClient method implementation:

func setupNexmoClient() {
    client = NXMClient(token: Constant.userToken)
    client?.setDelegate(self)
    client?.login()
}

Notice that self is set to be the delegate for NXMClient. Do not forget to adopt the NXMClientDelegate protocol and implement the required methods.

Add the required protocol adoption declaration to the class extension located towards the end of the ViewController.swift file:

extension ViewController: NXMClientDelegate {
    ...
}

The connectionStatusChanged:reason methods of the NXMClientDelegate protocol indicates if the login was successful and you can start using the SDK.

Add the following method under the //MARK:- Client Delegate line.

extension ViewController: NXMClientDelegate {

    func connectionStatusChanged(_ status: NXMConnectionStatus, reason: NXMConnectionStatusReason) {
        updateInterface()
    }

}

Open ViewController.m. Explore the setup methods that were written for you on viewDidLoad.

Now locate the following line #pragma mark - Tutorial Methods and complete the setupNexmoClient method implementation:

- (void)setupNexmoClient {
    self.nexmoClient = [[NXMClient alloc] initWithToken:kJaneToken];
    [self.nexmoClient setDelegate:self];
    [self.nexmoClient login];
}

Notice that self is set to be the delegate for NXMClient. Do not forget to adopt the NXMClientDelegate protocol and implement the required methods.

Add the required protocol adoption declaration to the class extension located in the ViewController.m file:

@interface ViewController () <NXMClientDelegate>

The NXMClientDelegate indicates if the login was successful and you can start using the SDK.

Add the following method under the #pragma mark NXMClientDelegate line.

- (void)connectionStatusChanged:(NXMConnectionStatus)status reason:(NXMConnectionStatusReason)reason {
    self.connectionStatus = status;
    [self updateInterface];
}

At this point you should already be able to run the app and see that you can login successfully with the SDK.

Start a call

You can now make an App-to-Phone call.

The Call button press is already connected to ViewController.

Implement the callNumber: method to start a call.

@IBAction func callNumber(_ sender: Any) {
    // call initiated but not yet active
    if callStatus == .initiated && call == nil {
        callStatus = .unknown
        self.call = nil
        updateInterface()
        return
    }
    // start a new call (check if a call already exists)
    guard let call = call else {
        startCall()
        return
    }
    // if a call exists, end it
    end(call: call)
}

If a call is already in progress, taping the button will end it.

Implement startCall - it will start the call, and also update the interface to show that a call is in progress:

private func startCall() {
    callStatus = .initiated
    client?.call(["CALLEE_PHONE_NUMBER"], callType: .server, delegate: self) { [weak self] (error, call) in
        guard let self = self else { return }
        // Handle create call failure
        guard let call = call else {
            if let error = error {
                // Handle create call failure
                print(" call not created: \(error.localizedDescription)")
            } else {
                // Handle unexpected create call failure
                print(" call not created: unknown error")
            }
            self.callStatus = .error
            self.call = nil
            self.updateInterface()
            return
        }
        // Handle call created successfully.
        // callDelegate's  statusChanged: will be invoked with needed updates.
        call.setDelegate(self)
        self.call = call
        self.updateInterface()
    }
    updateInterface()
}

Implement the callNumber: method to start a call.

- (IBAction)callNumber:(id)sender {
    if(!self.ongoingCall) {
        [self startCall];
    } else {
        [self endCall];
    }
}

If a call is already in progress, taping the button will end it.

Implement the startCall method to start a call. It will start the call, and also update the interface to show that a call is in progress:

- (void)startCall {
    if(self.ongoingCall) {
        return;
    }
    self.statusLabel.text = @"Calling...";
    [self.loadingIndicator startAnimating];
    self.callButton.alpha = 0;
    [self.nexmoClient call:@[@"CALLEE_NUMBER"] callType:NXMCallTypeServer delegate:self completion:^(NSError * _Nullable error, NXMCall * _Nullable call) {
        if(error) {
            NSLog(@" call not created: %@", error);
            self.ongoingCall = nil;
            [self updateInterface];
            return;
        }
        NSLog(@"🤙🤙🤙 call: %@", call);
        self.ongoingCall = call;
        self.ongoingCall.delegate = self;
        [self updateInterface];
    }];
}

You are expected to replace CALLEE_PHONE_NUMBER with the number to be called. But, ultimately, the number that is actually called is the one supplied in the Answer URL webhook. In a real-life use case, you would create a server component to serve as the Answer URL. The app will send to your backend, through the Answer URL the CALLEE_PHONE_NUMBER, the backend would validate it and then supply it in the JSON returned.

Note: Whilst the default HTTP method for the Answer URL is GET, POST can also be used.

Call Type

Note the use of NXMCallTypeServer as the callType in the client's call: method above; this specifies that the logic of the call is defined by the server - a requirement for outbound PSTN calls.

The other callType is NXMCallTypeInApp, useful for making simple calls as shown in this tutorial.

client?.call([calees], callType: .inApp, delegate: self) { [weak self] (error, call) in
    ...
}
[self.nexmoClient call:@[calees] callType:NXMCallTypeInApp delegate:self completion:^(NSError * _Nullable error, NXMCall * _Nullable call) {
    ...
}];

Call Delegate

As with NXMClient, NXMCall also has a delegate. You will now adopt the NXMCallDelegate as an extension on ViewController:

extension ViewController: NXMCallDelegate {

}

Copy the following implementation for the statusChanged method of the NXMCallDelegate along with the aid methods under the //MARK:- Call Delegate line:

extension ViewController: NXMCallDelegate {

    func statusChanged(_ member: NXMCallMember) {

        guard let call = call else {
            // this should never happen
            self.callStatus = .unknown
            self.updateInterface()
            return
        }

        // call ended before it could be answered
        if member == call.myCallMember, member.status == .answered, let otherMember = call.otherCallMembers.firstObject as? NXMCallMember, [NXMCallMemberStatus.completed, NXMCallMemberStatus.cancelled].contains(otherMember.status)  {
            self.callStatus = .completed
            self.call?.myCallMember.hangup()
            self.call = nil
        }

        // call rejected
        if call.otherCallMembers.contains(member), member.status == .cancelled {
            self.callStatus = .rejected
            self.call?.myCallMember.hangup()
            self.call = nil
        }

        // call ended
        if call.otherCallMembers.contains(member), member.status == .completed {
            self.callStatus = .completed
            self.call?.myCallMember.hangup()
            self.call = nil
        }

        updateInterface()
    }

}

As with NXMClient, NXMCall also receives a delegate which you supplied in the call:callType:delegate:completion: method.

You will now adopt the NXMCallDelegate for ViewController:

@interface ViewController () <NXMClientDelegate, NXMCallDelegate>

Copy the following implementation for the statusChanged method of the NXMCallDelegate along with the aid methods under the #pragma mark NXMCallDelegate line:

- (void)statusChanged:(NXMCallMember *)callMember {
    if (![callMember.user.userId  isEqual: kJaneUserId]) {
        self.callStatus = callMember.status;
    }
    //Handle Hangup
    if(callMember.status == NXMCallMemberStatusCancelled || callMember.status == NXMCallMemberStatusCompleted) {
        self.ongoingCall = nil;
        self.callStatus = NXMCallStatusDisconnected;
    }
    [self updateInterface];
}

The statusChanged: method notifies on changes that happens to members on the call.

Hangup a call

Once the "End Call" button is pressed, it is time to hangup the call.

Implement the private end: method and call hangup for myCallMember.

private func end(call: NXMCall) {
    call.myCallMember.hangup()
}

Implement endCall method and call hangup for myCallMember.

- (void)endCall {
    [self.loadingIndicator startAnimating];
    self.callButton.alpha = 0;
    [self.ongoingCall.myCallMember hangup];
}

Updates for callMember statuses are received in statusChanged as part of the NXMCallDelegate as you have seen before.

The existing implementation for statusChanged: is already handling call hangup.

Handle permissions

For the call to happen, Audio Permissions are required. In the appDelegate of the sample project, you can find an implementation for the permissions request in application:didFinishLaunchingWithOptions.

To read more about the permissions required, see the setup tutorial.

Conclusion

You have implemented your first App to Phone Voice application with the Nexmo Client SDK for iOS.

Run the app on a simulator and see that you can place and hangup a call to a PSTN phone number from the phone number associated with your Nexmo application.

If possible, test on a device using your developer signing and provisioning facility.