How to Make and Receive In App Calls with the Nexmo Client SDK on iOS

In this tutorial you learn how to use Nexmo Client SDK for iOS, in order to perform an in-app (IP to IP) voice call.

You will create a simple app to make a call and receive a call.

The app will have two buttons, which log in different users: Jane or Joe. After logging in, Jane and Joe are able to place a call and perform actions such as answer, reject or hangup.

Prerequisites

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 IDs and tokens:

        var userId: String {
            switch self {
            case .jane:
                return "" //TODO: swap with Jane's userId
            case .joe:
                return "" //TODO: swap with Joe's userId
            }
        }
    
        var token: String {
            switch self {
            case .jane:
                return "" //TODO: swap with a token for Jane
            case .joe:
                return "" //TODO: swap with a token for Joe
            }
        }
    
  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.

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

  1. Open IAVAppDefine.h file and replace the user IDs and tokens:

        #define kInAppVoiceJaneUserId @"JANE_USER_ID" //TODO: replace with a userId for Jane
        #define kInAppVoiceJaneToken @"JANE_TOKEN" //TODO: replace with a token for Jane
        #define kInAppVoiceJoeUserId @"JOE_USER_ID" //TODO: replace with a userId for Joe
        #define kInAppVoiceJoeToken @"JOE_TOKEN" //TODO: replace with a token for Joe
    
  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: user.token)
    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 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:self.selectedUser.token];
    [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 setWithConnectionStatus:status];
}

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 a simple In-App call. If you logged in as Jane, you can call Joe, and if you logged in as Joe you can call Jane - the call button changes its text accordingly.

Call Jane/Joe button press is already connected to the MainViewController.

Implement the call: method to start a call.

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

private func startCall() {

}
private func end(call: NXMCall) {

}

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

Let's implement startCall - it will start the call, and also update the visual elements so that Jane or Joe know the call is in progress:

private func startCall() {
    callStatus = .initiated
    client?.call([user.callee.userId], callType: .inApp, 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()
}

NB: You can have multiple users in a call (client?.call method takes an array as its first argument). However, this tutorial demonstrates a 1-on-1 call.

Call Other button press is already connected to the MainViewController.

Implement the didCallOtherButtonPress: method to start a call. It will start the call, and also update the UIViews so that Jane or Joe know the call is in progress:

- (IBAction)didCallOtherButtonPress:(UIButton *)sender {
    self.isInCall = YES;
    [self.nexmoClient call:@[self.otherUser.userId] callType:NXMCallTypeInApp delegate:self completion:^(NSError * _Nullable error, NXMCall * _Nullable call) {
        if(error) {
            self.isInCall = NO;
            self.ongoingCall = nil;
            [self updateCallStatusLabelWithText:@""];
            return;
        }
        self.ongoingCall = call;
        [self setActiveViews];
    }];
}

Ensure that NSArray is initialized with otherUser.userId. You can have multiple users in a call. However, this tutorial demonstrates a 1-on-1 call.

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

Adopt the NXMCallDelegate. Your extension declaration should look like this:

@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 {
    if([callMember.userId isEqualToString:self.selectedUser.userId]) {
        [self statusChangedForMyMember:callMember];
    } else {
        [self statusChangedForOtherMember:callMember];
    }
}

- (void)statusChangedForMyMember:(NXMCallMember *)myMember {
    [self updateCallStatusLabelWithStatus:myMember.status];

    //Handle Hangup

}

- (void)statusChangedForOtherMember:(NXMCallMember *)otherMember {

}

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

The statusChangedForOtherMember and statusChangedForMyMember methods are updated later when you will handle call hangup.

You can build the project now and make an outgoing call. Next you implement how to receive an incoming call.

Note that while NXMCallTypeInApp is useful for simple calls, you can also start a call with customized logic using an NCCO ), by choosing NXMCallTypeServer as the callType.

 [self.nexmoClient call:@[callees] callType:NXMCallTypeServer delegate:self completion:^(NSError * _Nullable error, NXMCall * _Nullable call){...}];

This also allows you to start a PSTN phone call, by adding a phone number to the callees array.

Call Type

Note the second parameter in the client?.call method above - while NXMCallType.inApp is useful for simple calls, you can also start a call with customized logic using an NCCO ), by choosing NXMCallType.server as the callType.

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

This also allows you to start a PSTN phone call, by adding a phone number to the callees array.

Call Delegate

As with NXMClient, NXMCall also receives a delegate supplied as the third argument in the call:callType:delegate:completion: method.

We'll now adopt the NXMCallDelegate as en 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 call.otherCallMembers.contains(member), member.status == .completed {
        self.callStatus = .completed
        self.call?.myCallMember.hangup()
        self.call = nil
    }

    updateInterface()
}

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

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 {
    if([callMember.user.userId isEqualToString:self.selectedUser.userId]) {
        [self statusChangedForMyMember:callMember];
    } else {
        [self statusChangedForOtherMember:callMember];
    }
}

- (void)statusChangedForMyMember:(NXMCallMember *)myMember {
    [self updateCallStatusLabelWithStatus:myMember.status];

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

- (void)statusChangedForOtherMember:(NXMCallMember *)otherMember {
    if(otherMember.status == NXMCallMemberStatusCancelled || otherMember.status == NXMCallMemberStatusCompleted) {
        [self.ongoingCall.myCallMember hangup];
    }
}

You can build the project now and make an outgoing call. Next you implement how to receive an incoming call.

Receive incoming call

When Jane calls Joe, Joe should be notified, so that Joe may decide to answer or reject the call.

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

Go back to the //MARK: NXMClientDelegate line and add the `incomingCall:' method

func incomingCall(_ call: NXMCall) {
    print("   Incoming Call: \(call)")
    DispatchQueue.main.async {
        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
    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];
    }];
}

The answer:completionHandler method accepts an object adopting the NXMCallDelegate, and a completionHandler, 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 - Reject, implement this method to reject the incoming call:

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

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

- (void)didPressRejectIncomingCall {
    __weak MainViewController *weakSelf = self;
    [weakSelf.ongoingCall reject:^(NSError * _Nullable error) {
        if(error) {
            [weakSelf displayAlertWithTitle:@"Reject Call" andMessage:@"Error rejecting call"];
            return;
        }

        weakSelf.ongoingCall = nil;
        weakSelf.isInCall = NO;
        [self updateCallStatusLabelWithText:@""];
        [weakSelf setActiveViews];
    }];
}

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

Hangup a call

Once Jane or Joe 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) {
    call.myCallMember.hangup()
    callStatus = .completed
    self.call = nil
    updateInterface()
}

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 or Joe presses the red button, it is time to hangup the call. Implement didEndButtonPress: method and call hangup for myCallMember.

- (IBAction)didEndButtonPress:(UIButton *)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 statusChangedForOtherMember and statusChangedForMyMember to handle call hangup:

- (void)statusChangedForMyMember:(NXMCallMember *)myMember {
    [self updateCallStatusLabelWithStatus:myMember.status];

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

- (void)statusChangedForOtherMember:(NXMCallMember *)otherMember {
    if(otherMember.status == NXMCallMemberStatusCancelled || otherMember.status == NXMCallMemberStatusCompleted) {
        [self.ongoingCall.myCallMember 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 In App Voice application with the Nexmo Client SDK for iOS.

Run the app on two simulators and see that you can call, answer, reject and hangup.

If possible, test on two phones using your developer signing and provisioning facility.