Getting started with Socket.io in Swift on iOS

September 30, 2016
Written by
Sam Agnew
Twilion

call-progress-events-blog-post

WebSockets are a powerful protocol for real-time event based communication. Socket.io makes working with WebSockets easier on iOS with Swift compared to implementing all the underlying bits yourself.

Let’s demonstrate how to work with the Swift Socket.io client library by building an application to monitor the status of Twilio phone calls. This application will be similar to the application built in another tutorial for getting started with Socket.io on the web with JavaScript.

The tools we’ll need

  • Node.js and npm version 4.0 or greater installed on your machine for our web server
  • Xcode 8.0 or greater and Swift 3.0 or greater

To find out which version of swift you’re running, enter the following command in your terminal:

xcrun swift -version

 

Setting up our server

Start off by cloning or downloading a zip file of this starter repository which contains two projects:

  • a prebuilt Node.js web app and server and
  • an iOS app with a pre-configured storyboard set up with a UITableView that we will use to display the data we receive.

To clone the repository open your terminal, navigate to where you want this project to live and enter the following commands:

git clone git@github.com:sagnew/CallStatusDashboard.git
cd CallStatusDashboard
git checkout iOS-tutorial

Once you have the repository cloned or the ZIP extracted, install the dependencies for the Node.js app:

npm install

Once the dependencies install open up index.js and add this new route at the bottom:

// Test route for socket events.
app.get('/test', (req, res) => {
  io.emit('test', { 'Hello': 'World' });
  res.send('Hello Socket.io :)');
});

This route uses Socket.io to emit an event named “test”. In just a bit we will configure our iOS app to receive that event later in the post.  For now, run the server by entering the following terminal command and keep it running for us to test later:

node index.js

Now let’s make our web server accessible over the internet so we can run our app. We need to do this because iOS requires apps that want to make requests to external services do it via HTTPS. We also need a publicly accessible URL later on to test phone calls made with the Twilio REST API. For this, we will use a tool called ngrok.

Download ngrok and have it run on port 3000 in a new terminal window with the following command:

./ngrok http 3000

Take note of the randomly generated URL that ngrok provides you as we’ll be using it in a bit. Visit this URL and add “/test” to the end of it to see the new route that we added to index.js.

Screen Shot 2016-09-30 at 4.21.21 PM.png

Setting up our client side iOS app

Navigate to the directory containing the starter iOS app. From the root directory of this repository run:

cd iOS/CallStatusDashboard

We’ll use CocoaPods to install the dependencies used by  our app. First, install CocoaPods if you don’t have it:

sudo gem install cocoapods

Now install the dependencies:

pod install

CocoaPods links the dependencies of our project together by creating a new Xcode Workspace. To make sure XCode loads those dependencies open the workspace using the CallStatusDashboard.xcworkspace file instead of the Xcode project file:

open CallStatusDashboard.xcworkspace

Lets verify the Socket.io pod was added to our project by importing it in ViewController.swift:

import SocketIO

Build the project and a prompt that says “Build Successful” should display.

Getting started with Socket.io in Swift

Now we’re ready to start receiving Socket.io events from our server.

Create a new Swift file called SocketIOManager.swift and add the following:

import SocketIO

class SocketIOManager: NSObject {
    static let sharedInstance = SocketIOManager()
    var socket = SocketIOClient(socketURL: URL(string: "https://your-ngrok-url.ngrok.io")!, config: [.log(false), .forcePolling(true)])
    
    override init() {
        super.init()

        socket.on("test") { dataArray, ack in
            print(dataArray)
        }

    }
    
    func establishConnection() {
        socket.connect()
    }
    
    func closeConnection() {
        socket.disconnect()
    }
}

This code creates a Singleton class so we can reuse the Socket.io functionality across our entire application. The init function tests our socket connection by subscribing to the “test” event emitted by our server.

Don’t forget to replace the socket URL with the URL that ngrok generated for you.

Head over to AppDelegate.swift and use SocketIOManager to connect to the server when the app becomes active and disconnect from it when the app enters the background or exits. To do this, add the following two lines where appropriate in AppDelegate.swift:

func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
        SocketIOManager.sharedInstance.closeConnection()
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
        SocketIOManager.sharedInstance.establishConnection()
    }

Run the app and open http://localhost:3000/test in your browser. Visiting this URL should cause the Node app to emit an event to our iOS app which will print the data array we receive in the Xcode console:

Screen Shot 2016-09-15 at 7.01.26 PM.png

Monitoring the status of a phone call

We’re ready to recreate the web app from my other Socket.io tutorial in our iOS app.

This previous post explained how to monitor phone calls sent with the Twilio API in real time using call progress events. With these, you can set up a webhook to receive a request whenever the status of your phone calls change. This is already taken care of in index.js.

The real time updates of call progress events provide us with a great opportunity to play around with WebSockets.

When the status of a phone call changes, our server will emit an event that the iOS app we are building will receive. From there, we’ll update the UITableView in our app to display the new status of that phone call.

We’ll need a data structure to represent phone calls. Let’s use a struct for this. Create a new file called PhoneCall.swift and add the following code:

struct PhoneCall {
    let callSid: String
    let toNumber: String
    let fromNumber: String
    var callStatus: String
    
    init(callSid: String, toNumber: String, fromNumber: String, callStatus: String) {
        self.callSid = callSid
        self.toNumber = toNumber
        self.fromNumber = fromNumber
        self.callStatus = callStatus
    }
}

With this struct we can create an array allowing us to store all of the PhoneCallss we want to show in our UITableView. Replace the global array of Strings inside the ViewController class of ViewController.swift with the following:

class ViewController: UITableViewController {
    let phoneCallCellIdentifier = "PhoneCallCell"
    var phoneCalls: [PhoneCall] = []
    
    @IBOutlet var callStatusTableView: UITableView!

And replace the code inside tableView(_:cellForRowAt:) with the following:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: phoneCallCellIdentifier, for: indexPath)
        
        let row = (indexPath as NSIndexPath).row
        let call = phoneCalls[row]
        
        cell.detailTextLabel?.text = "\(call.fromNumber) -> \(call.toNumber): \(call.callStatus)"
        cell.textLabel?.text = call.callSid
        
        return cell
    }

In SocketIOManager.swift, add the following code to the end of the init function. This will listen for status update events sent from the Node app similarly to the test event we created before. On receiving this event, we will pass a message to the rest of the  code using NSNotificationCenter:

override init() {
        super.init()
        
        socket.on("test") { dataArray, ack in
            print(dataArray)
        }
        
        socket.on("status update") { dataArray, ack in
            NotificationCenter.default
                .post(name: Notification.Name(rawValue: "callStatusUpdateNotification"), object: dataArray[0] as? [String: AnyObject])
        }
        
    }

We need to modify ViewController.swift to receive that status update and update the UITableView. If it is a new call, we’ll add it to the phoneCalls array. If it is an existing call with an updated status, we will update the corresponding array element.

Create a new function in ViewController called handleCallStatusUpdateNotification with the following code:

func handleCallStatusUpdateNotification(_ notification: Notification) {
        if let data = notification.object as? [String: String],
                let callSid = data["callSid"], let toNumber = data["to"],
                let fromNumber = data["fromNumber"], let callStatus = data["callStatus"] {
            let newPhoneCall = PhoneCall(callSid: callSid, toNumber: toNumber, fromNumber: fromNumber, callStatus: callStatus)
            var isNewCall = true
            
            self.phoneCalls = self.phoneCalls.map({ phoneCall -> PhoneCall in
                if phoneCall.callSid == newPhoneCall.callSid {
                    // This is the updated phone call.
                    isNewCall = false
                    return newPhoneCall
                }
                
                // This is an unchanged phone call.
                return phoneCall
            })
            
            if isNewCall {
                self.phoneCalls.append(newPhoneCall)
            }
        
            self.callStatusTableView.reloadData()
        }
    }

Next subscribe to the notification and set our previous function as it’s handler by adding this code to the end of viewDidLoad:

override func viewDidLoad() {
        super.viewDidLoad()
        self.callStatusTableView.delegate = self
        self.callStatusTableView.dataSource = self
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleCallStatusUpdateNotification(_:)), name: NSNotification.Name(rawValue: "callStatusUpdateNotification"), object: nil)
    }

Finally, In order to test everything, you’ll need a Twilio phone number. You can buy one here or just use a trial number that comes with your Twilio account. When using a trial number, you can only make calls to numbers that you verified. This shouldn’t be a problem for now though.

Open makeCall.js in the root directory of our project and replace all of the relevant values with your Twilio phone number, your personal phone number and your ngrok URL:

client.makeCall({
  url: 'http://YOUR_NGROK_URL/voice',
  to: 'YOUR_PHONE_NUMBER',
  from: 'YOUR_TWILIO_PHONE_NUMBER',
  statusCallback: 'http://YOUR_NGROK_URL/events',
  statusCallbackMethod: 'POST',
  statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'],
}, (err, call) => {
  if(err) { console.log(err); return err; }
  process.stdout.write(call.sid);
});

Now run the app, and enter the following in your terminal to see a phone call appear in the table in your iOS app:

node makeCall.js

This will use the Twilio REST API to make your phone receive a call from your Twilio number. You should see the status of the call displayed in your iOS app changing when you answer and hang up.

Try it a few times to see it working!

Simulator Screen Shot Sep 30, 2016, 4.42.21 PM.png

Real time communication is rad

Now you can add real time events to communicate between client and server to any of the apps you build. If you want to see how to build a similar application, but in a web browser, check out this ReactJS tutorial that also uses Socket.io to monitor the status of phone calls.

I can’t wait to see what kind of awesome projects you build. Check out Twilio’s iOS Client and documentation for some ideas.

Feel free to reach out for any questions: