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

In this guide, you'll learn how to receive an incoming phone call in a iOS application.

You will create a simple iOS app, that will automatically log in a user called Jane. After logging in, Jane is able to receive a call and perform actions such as answer, reject or hangup.

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 connect an incoming phone call to an app user, you'll need to provide a URL as the Answer URL webhook - we've created a gist for you to use.

To add this URL, go to your Nexmo dashboard, navigate to applications, select your application and click the 'Edit' button.

Now, set the application's Answer URL to:

https://gist.githubusercontent.com/NexmoDev/ed91ac99a0b278fbdcbde72ca3599ac7/raw/4a134363f8b3bbebae27f04095a57d0cebc5a1be/ncco.json

and click 'Save changes'.

NOTE: This gist is specific to this tutorial and in a real-life scenario, the answer_url should be provided by a purposely built web solution that can serve custom NCCOs if required.

A Nexmo virtual number Nexmo phone number is a phone number that you can link to your Nexmo Application. When a user calls that phone number, the answer_url that is defined on your Nexmo Application is executed. In this tutorial case, you set up an answer_url in the above step, that upon calling connects the call to your app user Jane.

To link a Nexmo number to your app:

  1. Go to your Nexmo dashboard
  2. Navigate to applications
  3. Select your application
  4. Click the 'Edit' button.

  5. Switch to Numbers tab above, search for a number you'd like, and click Link, to link.

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 MainViewController.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 token:

        #define kInAppVoiceJaneToken @"JANE_TOKEN" //TODO: replace with a token for Jane
    
  2. Open MainViewController.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 MainViewController.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 MainViewController.swift file:

extension MainViewController: 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 #pragma mark NXMClientDelegate line.

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

Open MainViewController.m. Explore the setup instructions 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:kInAppVoiceJaneToken];
    [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 MainViewController.m file:

@interface MainViewController () <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.

Receive incoming call

When the phone number associated with your Nexmo app receives a call, the app should notify the user Jane so that she can decide whether to answer or reject it.

This is done by implementing the optional incomingCall: method which is declared in the NXMClientDelegate.

Go back to the #pragma mark NXMClientDelegate line and add the `incomingCall:' method

func incomingCall(_ call: NXMCall) {
    print("📲 📲 📲 Incoming Call: \(call)")
    DispatchQueue.main.async { [weak self] in
        self?.displayIncomingCallAlert(call: call)
    }
}

Go back to the #pragma mark NXMClientDelegate line and add the `incomingCall:' method

- (void)incomingCall:(nonnull NXMCall *)call {
    self.ongoingCall = call;
    [self displayIncomingCallAlert];
}

This method takes as a parameter an NXMCall object with which you can answer or reject the call. An alert was implemented for you, to allow the user to choose whether to answer or reject the call.

Answer a call

Under the //MARK: Incoming call - Accept, implement this method to answer the incoming call:

private func answer(call: NXMCall) {
    self.call = call
    self.call?.setDelegate(self)
    call.answer(self) { [weak self] error in
        if let error = error {
            print("error answering call: \(error.localizedDescription)")
        }
        self?.updateInterface()
    }
}

Under the #pragma mark IncomingCall, implement this method to answer the incoming call:

- (void)didPressAnswerIncomingCall {
    __weak MainViewController *weakSelf = self;
    [weakSelf.ongoingCall answer:self completionHandler:^(NSError * _Nullable error) {
        if(error) {
            [weakSelf displayAlertWithTitle:@"Answer Call" andMessage:@"Error answering call"];
            weakSelf.ongoingCall = nil;
            weakSelf.isInCall = NO;
            [self updateCallStatusLabelWithText:@""];
            [weakSelf setActiveViews];
            return;
        }
        self.isInCall = YES;
        [weakSelf setActiveViews];
    }];
}

answer:completionHandler: accepts an object adopting the NXMCallDelegate and a completion block to indicate if an error occurred in the process. You already implemented NXMCallDelegate in a previous step.

Reject a call

Under the //MARK: Incoming call - Accept, implement this method to answer the incoming call:

private func answer(call: NXMCall) {
    self.call = call
    self.call?.setDelegate(self)
    call.answer(self) { [weak self] error in
        if let error = error {
            print("error answering call: \(error.localizedDescription)")
        }
        self?.updateInterface()
    }
}

Under the #pragma mark IncomingCall, implement this method to answer the incoming call:

- (void)didPressAnswerIncomingCall {
    __weak MainViewController *weakSelf = self;
    [weakSelf.ongoingCall answer:self completionHandler:^(NSError * _Nullable error) {
        if(error) {
            [weakSelf displayAlertWithTitle:@"Answer Call" andMessage:@"Error answering call"];
            weakSelf.ongoingCall = nil;
            weakSelf.isInCall = NO;
            [self updateCallStatusLabelWithText:@""];
            [weakSelf setActiveViews];
            return;
        }
        self.isInCall = YES;
        [weakSelf setActiveViews];
    }];
}

reject: accepts a single completionHandler parameter to indicate if an error occurred in the process.

Call Delegate

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

extension MainViewController: NXMCallDelegate {

}

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

func statusChanged(_ member: NXMCallMember!) {
    print("🤙🤙🤙 Call Status changed | member: \(String(describing: member.user.name))")
    print("🤙🤙🤙 Call Status changed | member status: \(String(describing: member.status.description()))")

    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 member.status == .completed {
        self.callStatus = .completed
        self.call?.myCallMember.hangup()
        self.call = nil
    }

    updateInterface()
}

As with NXMClient, NXMCall also has a delegate. Add the required protocol adoption declaration to the class extension located in the MainViewController.m file:

@interface MainViewController () <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 {
    [self updateCallStatusLabelWithStatus:callMember.status];
}

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

Hangup a call

Once Jane presses the 'End Call' button, it is time to hangup the call. Implement the private end: method and call hangup for myCallMember.

private func end(call: NXMCall) {
    guard let call = call else {
        updateInterface()
        return
    }
    call.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.

Once Jane presses the "End Call" button, it is time to hangup the call. Implement endCall: method and call hangup for myCallMember.

- (IBAction)endCall:(id)sender {
    [self.ongoingCall.myCallMember hangup];
}

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

Update the implementation for statusChanged to handle call hangup:

- (void)statusChanged:(NXMCallMember *)callMember {
    [self updateCallStatusLabelWithStatus:callMember.status];

    //Handle Hangup
    if(callMember.status == NXMCallMemberStatusCancelled || callMember.status == NXMCallMemberStatusCompleted) {
        self.ongoingCall = nil;
        self.isInCall = NO;
        [self updateCallStatusLabelWithText:@"Call ended"];
        [self updateInterface];
    }
}

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

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

Run the app on a simulator, and with another device call the nexmo Number you linked. Then, see that you can answer, reject and hangup a call received on the phone number associated with your Nexmo application.

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