When building chat applications, it is not uncommon to see something like a friends’ list with the status of the friend. Applications like WhatsApp have this feature and it is very useful to check the status of your friend and know if it is wise to send them a message at that point.
We are going to be building a similar feature in a make-believe iOS chat application. We are going to be using Pusher to implement realtime features to the application so that when someone posts a new status update you can see it change in realtime.
Here is a screen recording of how our application will look when we are done.
To get started, we need to create the iOS project and then install some dependencies that’ll be needed for the application to function correctly. Let us begin.
Launch Xcode on your machine and create a new project. Create a single application project and follow the wizard until you get to the main storyboard. Once you are there, exit Xcode.
In your terminal, cd
to the Xcode project directory and then run the command below:
$ pod init
This will create a Podfile
inside the root of your application. The Podfile is where we will define Cocoapods dependencies. Open in your text editor and replace with the following:
1platform :ios, '8.4' 2 3 target 'project_name' do 4 use_frameworks! 5 pod 'PusherSwift', '~> 4.0' 6 pod 'Alamofire', '~> 4.4' 7 end
In the above, we have just specified the dependencies we want CocoaPods to install into our application. Do not forget to substitute the project_name for your actual project name.
Now go to the terminal and run the command:
$ pod install
This should install all of the dependencies and libraries we have specified in our Podfile
. Great! Finally, open the project directory and double-click the .xcworkspace
file in the directory to launch your project workspace in Xcode.
Now that we have created the project in Xcode and have successfully installed all the dependencies, the next thing we will want to do is create the user interface of our iOS application. Open the main.storyboard
file in Xcode and let’s start designing the UI.
This is what we want to have at the end of this section:
Add a Navigation Controller in your canvas and make it the root view controller. When you have done this, you then need to update the TableViewController
attached to the Navigation Controller.
First, create a new class in Xcode using ctrl+n
; the class name should be FriendsViewController
and it should extend UITableViewController
. Then, in the main.storyboard
file, make sure you make the TableViewController
use the FriendsViewController
as its custom class.
Now that we have created the table view controller, we need to configure its cells to match what we are trying to achieve.
Click on “Prototype Cells” on the main storyboard file and make the attributes inspector look something close to the image below.
💡 For the image
**avatar.png**
, you can add a 45x45 pixel image to your Xcode project and use that as the image for the cell.
One last thing we can do (which is completely optional) is changing the navigation bar color for our application. Let’s do that.
Open the AppDelegate
class and in the application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?)
method paste the following:
1UINavigationBar.appearance().barTintColor = UIColor(red: 18.0/255.0, green: 140.0/255.0, blue: 126.0/255.0, alpha: 1.0) 2 UINavigationBar.appearance().tintColor = UIColor.white 3 UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white]
With this, you have finished creating the UI for the application and all that is left is the functionality to support it. Let us do that now.
For the functionality, we will divide it into two parts. The first part will focus on adding the functionality for updating the status, and the second part will focus on making the updates realtime.
Lets open FriendsViewController
and make some modifications. The first modification will be adding an update “Status” button to the top right corner of the navigation bar.
Inside the viewDidLoad
method of the controller, add the code below:
1navigationItem.title = "Friends List" 2 navigationItem.rightBarButtonItem = UIBarButtonItem( 3 title: "Status", 4 style: .plain, 5 target: self, 6 action: #selector(showPopup(_:)) 7 )
The code above simply sets the title of the controller in the navigation bar and adds a button to the right side of the navigation bar.
If you notice, in the action
parameter it points to a method showPopup
so let us create this method. Add this method to the controller:
1public func showPopup(_ sender: Any) { 2 let alertController = UIAlertController( 3 title: "Update your status", 4 message: "What would you like your status to say?", 5 preferredStyle: .alert 6 ) 7 8 alertController.addTextField(configurationHandler: {(_ textField: UITextField) -> Void in 9 textField.placeholder = "Status" 10 }) 11 12 alertController.addAction(UIAlertAction(title: "Update", style: .default, handler: {(_ action: UIAlertAction) -> Void in 13 let status = (alertController.textFields?[0].text)! as String 14 self.postStatusUpdate(message: status) 15 })) 16 17 alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 18 19 present(alertController, animated: true, completion: nil) 20 }
So what we did here is, when the action is called and the showPopup
method is invoked, the application will display a popup box that asks the user to input their status.
Right now, the popup calls a method postStatusUpdate
which does not exist in our application. Let us create this method now.
In the view controller, add the method below:
1public func postStatusUpdate(message: String) { 2 let params: Parameters = ["username": username, "status": message] 3 4 Alamofire.request(FriendsViewController.API_ENDPOINT + "/status", method: .post, parameters: params).validate().responseJSON { response in 5 switch response.result { 6 7 case .success: 8 _ = "Updated" 9 case .failure(let error): 10 print(error) 11 } 12 } 13 }
In this method, we are using the Alamofire
library to make a request to an endpoint FriendsViewController.API_ENDPOINT + "/status``"
(which does not yet exist). Right now, because we have not imported the Alamofire library nor defined FriendsViewController.API_ENDPOINT
we will get errors.
At the top of the view controller, import the Alamofire
library:
import 'Alamofire'
Also, inside the class, after the class definition, add the following to declare the API_ENDPOINT
which will point to the remote HTTP server.
static let API_ENDPOINT = "http://localhost:4000";
💡 The endpoint we are using now is a local server which will be created later on in the article. If you are using a remote server, you will need to replace this value with the URL of your server.
So, right now, when you run the application and click the “Status” button it will bring a popup and you can enter your update. However, because we have not yet created a backend to respond to this call, it will fail and not do anything. We will get to that later in the article.
The table view controller comes with some methods by default, and we will quickly change them to fit our application.
Open the view controller and update the method numberOfSections
. Make the return value 1. This will make sure that the first and only section is displayed.
Next, update the tableView(tableView: UITableView, numberOfRowsInSection: section)
method and make the return value friends.count
. This will make sure that the right amount of rows are created for each entry on the friends
list.
To make the cells display the details of each friend, update the contents of the tableView(tableView:UITableView, cellForRowAt indexPath:IndexPath)
method with the code below:
1let cell = tableView.dequeueReusableCell(withIdentifier: "friends", for: indexPath) 2 3 var status = friends[indexPath.row]["status"] 4 5 if status == "" { 6 status = "User has not updated status!" 7 } 8 9 cell.detailTextLabel?.textColor = UIColor.gray 10 11 cell.imageView?.image = UIImage(named: "avatar.png") 12 cell.textLabel?.text = friends[indexPath.row]["username"] 13 cell.detailTextLabel?.text = status 14 15 return cell
The code above simply gets the current cell and updates the required cell labels with the status, username and image (in case you want to add another image).
Finally, add a new method to the view controller:
1override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 2 return 75.0 3 }
This will just increase the row height of the table to be equal to 75.0. This will make it easier to accommodate the contents of the cell.
Now, before we add the realtime online status updates using Pusher, we want to add some sort pseudo friends listing.
We will do the friends listing using Pusher. We will accomplish this by creating a class property that is not persistent, and in this variable, we will store details of anyone that comes online.
In the view controller, add some new properties:
1var friends : [[String:String]] = [] 2 var username : String = "" 3 var pusher : Pusher!
The friends
property will store all the users who come online, the username
property will store a random username for the current user, and the pusher
property will store the Pusher library instance.
Now, in the viewDidLoad
method, add the following code:
1username = "Anonymous" + String(Int(arc4random_uniform(1000))) 2 3 listenForRealtimeEvents() 4 5 // --- Update online presence at intervals --- // 6 let date = Date().addingTimeInterval(0) 7 let timer = Timer(fireAt: date, interval: 1, target: self, selector: #selector(postOnlinePresence), userInfo: nil, repeats: true) 8 RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
On line 1, we just assign the username
property a random string as a username.
On line 3, we call a method listenForRealtimeEvents
that does not yet exist (we will create this later).
And on line 6 - 8, we just basically added a looping call to the postOnlinePresence
(also doesn’t exist yet). This call will basically update your online presence every second.
Let us create the listenForRealtimeEvents
method now. Add the following code to the view controller:
1private func listenForRealtimeEvents() { 2 pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(host: .cluster("PUSHER_CLUSTER"))) 3 4 let channel = pusher.subscribe("new_status") 5 let _ = channel.bind(eventName: "online", callback: { (data: Any?) -> Void in 6 if let data = data as? [String: AnyObject] { 7 let username = data["username"] as! String 8 9 let index = self.friends.index(where: { $0["username"] == username }) 10 11 if username != self.username && index == nil { 12 self.friends.append(["username": username, "status": "No Status"]) 13 self.tableView.reloadData() 14 } 15 } 16 }) 17 18 pusher.connect() 19 }
In the method we just created, we just instantiated the Pusher library with our Pusher key and application cluster. Then we subscribed to a Pusher channel called new_status and, on that channel, we started listening for the event called online.
In the callback, when the event listener is triggered, we get the username from the event. We then check if there is username in the list of friends
that matches. If there isn’t, we append it to the friends’ list and reload the table data.
So, in summary, every time someone comes online it appends the name to the friends’ list and reloads the table view.
Next, we will create the method postOnlinePresence
that will periodically post the current users online presence so others can pick it up. In the view controller add the code below:
1public func postOnlinePresence() { 2 let params: Parameters = ["username": username] 3 4 Alamofire.request(FriendsViewController.API_ENDPOINT + "/online", method: .post, parameters: params).validate().responseJSON { response in 5 switch response.result { 6 7 case .success: 8 _ = "Online" 9 case .failure(let error): 10 print(error) 11 } 12 } 13 }
The code above simply hits an endpoint and thus marks the user as online.
The final part of our iOS application will be adding the listener for the updates so that every time someone updates their status, the update is added.
To do this, open the listenForRealtimeEvents
method and add the following after instantiating the pusher variable:
1let channel = pusher.subscribe("new_status") 2 let _ = channel.bind(eventName: "update", callback: { (data: Any?) -> Void in 3 if let data = data as? [String: AnyObject] { 4 let username = data["username"] as! String 5 6 let status = data["status"] as! String 7 8 let index = self.friends.index(where: { $0["username"] == username }) 9 10 if index != nil { 11 self.friends[index!]["status"] = status 12 self.tableView.reloadData() 13 } 14 } 15 })
The above code creates a listener for the update event to the new_status channel. When the event is triggered, the callback checks if the username is part of the friends’ list. If it is, it updates the status of that entry and reloads the table view data.
Now we have successfully added the realtime features to our application. The next thing we want to do is create a backend to help us actually trigger Pusher events that can be picked up by our iOS application.
Create a directory for the web application and then create some new files:
1**index.js** 2 3 // ------------------------------------------------------ 4 // Import all required packages and files 5 // ------------------------------------------------------ 6 7 let Pusher = require('pusher'); 8 let express = require('express'); 9 let app = express(); 10 let bodyParser = require('body-parser') 11 12 let pusher = new Pusher(require('./config.js')); 13 14 // ------------------------------------------------------ 15 // Set up Express middlewares 16 // ------------------------------------------------------ 17 18 app.use(bodyParser.json()); 19 app.use(bodyParser.urlencoded({ extended: false })); 20 21 // ------------------------------------------------------ 22 // Define routes and logic 23 // ------------------------------------------------------ 24 25 app.post('/status', (req, res, next) => { 26 let payload = {username: req.body.username, status: req.body.status}; 27 pusher.trigger('new_status', 'update', payload); 28 res.json({success: 200}); 29 }); 30 31 app.post('/online', (req, res, next) => { 32 let payload = {username: req.body.username}; 33 pusher.trigger('new_status', 'online', payload); 34 res.json({success: 200}); 35 }); 36 37 app.get('/', (req, res) => { 38 res.json("It works!"); 39 }); 40 41 42 // ------------------------------------------------------ 43 // Catch errors 44 // ------------------------------------------------------ 45 46 app.use((req, res, next) => { 47 let err = new Error('Not Found: '); 48 err.status = 404; 49 next(err); 50 }); 51 52 53 // ------------------------------------------------------ 54 // Start application 55 // ------------------------------------------------------ 56 57 app.listen(4000, () => console.log('App listening on port 4000!'));
In this file, we have created a basic Express application. The application has two important endpoints: POST /online
and POST /status
. They both trigger Pusher events with a payload which will be picked up by listeners in our iOS application.
Next create the config.js file:
1module.exports = { 2 appId: 'PUSHER_ID', 3 key: 'PUSHER_KEY', 4 secret: 'PUSHER_SECRET', 5 cluster: 'PUSHER_CLUSTER', 6 };
This is our Pusher configuration file. In here, replace the empty strings with the credentials provided in your Pusher dashboard.
Finally, create a package.json file:
1{ 2 "main": "index.js", 3 "dependencies": { 4 "body-parser": "^1.16.0", 5 "express": "^4.14.1", 6 "pusher": "^1.5.1" 7 } 8 }
This file contains all the node packages required for the Node app to function properly.
Finally, in the directory of your Node application, run the command below:
$ npm install && node index.js
The first command will install all the dependencies and the second one will start an Express server in node. When you see the message “App listening on port 4000!” **then you know your backend application is ready.
Once you have your local node web server running, you will need to make some changes so your application can talk to the local web server. In the info.plist
file, make the following changes:
With this change, you can build and run your application and it will talk directly with your local web application.
In the article, we have been able to create an iOS application with realtime user status updates, similar to an application like WhatsApp currently has.