How to build a realtime user’s status update in iOS
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 follow along with this article, you must have the following requirements:
– Some knowledge of Swift 3
– Knowledge on how to use Xcode
– Basic JavaScript knowledge
– NPM and CocoaPods installed on your machine.
– Basic knowledge on the terminal (command-line)
– A Pusher application (You will need the application’s ID, Secret, Key, and Cluster). Create a Pusher account if you do not have one currently.
Getting our project ready
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.
Setting up our project in Xcode
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:
platform :ios, '8.4'
target 'project_name' do
use_frameworks!
pod 'PusherSwift', '~> 4.0'
pod 'Alamofire', '~> 4.4'
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.
Creating the User Interface of our realtime iOS application
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.
Configuring the prototype cells
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 45×45 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:
UINavigationBar.appearance().barTintColor = UIColor(red: 18.0/255.0, green: 140.0/255.0, blue: 126.0/255.0, alpha: 1.0)
UINavigationBar.appearance().tintColor = UIColor.white
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.
Creating the functionality of our realtime iOS application
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.
Creating the initial functionality: U****pdate status
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:
navigationItem.title = "Friends List"
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "Status",
style: .plain,
target: self,
action: #selector(showPopup(_:))
)
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:
public func showPopup(_ sender: Any) {
let alertController = UIAlertController(
title: "Update your status",
message: "What would you like your status to say?",
preferredStyle: .alert
)
alertController.addTextField(configurationHandler: {(_ textField: UITextField) -> Void in
textField.placeholder = "Status"
})
alertController.addAction(UIAlertAction(title: "Update", style: .default, handler: {(_ action: UIAlertAction) -> Void in
let status = (alertController.textFields?[0].text)! as String
self.postStatusUpdate(message: status)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertController, animated: true, completion: nil)
}
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:
public func postStatusUpdate(message: String) {
let params: Parameters = ["username": username, "status": message]
Alamofire.request(FriendsViewController.API_ENDPOINT + "/status", method: .post, parameters: params).validate().responseJSON { response in
switch response.result {
case .success:
_ = "Updated"
case .failure(let error):
print(error)
}
}
}
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.
Updating the default table view controller methods
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:
let cell = tableView.dequeueReusableCell(withIdentifier: "friends", for: indexPath)
var status = friends[indexPath.row]["status"]
if status == "" {
status = "User has not updated status!"
}
cell.detailTextLabel?.textColor = UIColor.gray
cell.imageView?.image = UIImage(named: "avatar.png")
cell.textLabel?.text = friends[indexPath.row]["username"]
cell.detailTextLabel?.text = status
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:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 75.0
}
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.
Using Pusher to add a realtime update status to our iOS application
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.
Adding a pseudo friends list
In the view controller, add some new properties:
var friends : [[String:String]] = []
var username : String = ""
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:
username = "Anonymous" + String(Int(arc4random_uniform(1000)))
listenForRealtimeEvents()
// --- Update online presence at intervals --- //
let date = Date().addingTimeInterval(0)
let timer = Timer(fireAt: date, interval: 1, target: self, selector: #selector(postOnlinePresence), userInfo: nil, repeats: true)
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:
private func listenForRealtimeEvents() {
pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(host: .cluster("PUSHER_CLUSTER")))
let channel = pusher.subscribe("new_status")
let _ = channel.bind(eventName: "online", callback: { (data: Any?) -> Void in
if let data = data as? [String: AnyObject] {
let username = data["username"] as! String
let index = self.friends.index(where: { $0["username"] == username })
if username != self.username && index == nil {
self.friends.append(["username": username, "status": "No Status"])
self.tableView.reloadData()
}
}
})
pusher.connect()
}
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:
public func postOnlinePresence() {
let params: Parameters = ["username": username]
Alamofire.request(FriendsViewController.API_ENDPOINT + "/online", method: .post, parameters: params).validate().responseJSON { response in
switch response.result {
case .success:
_ = "Online"
case .failure(let error):
print(error)
}
}
}
The code above simply hits an endpoint and thus marks the user as online.
Adding status updates to the application with Pusher
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:
let channel = pusher.subscribe("new_status")
let _ = channel.bind(eventName: "update", callback: { (data: Any?) -> Void in
if let data = data as? [String: AnyObject] {
let username = data["username"] as! String
let status = data["status"] as! String
let index = self.friends.index(where: { $0["username"] == username })
if index != nil {
self.friends[index!]["status"] = status
self.tableView.reloadData()
}
}
})
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.
Creating the NodeJS backend for our realtime iOS status updates app
Create a directory for the web application and then create some new files:
**index.js**
// ------------------------------------------------------
// Import all required packages and files
// ------------------------------------------------------
let Pusher = require('pusher');
let express = require('express');
let app = express();
let bodyParser = require('body-parser')
let pusher = new Pusher(require('./config.js'));
// ------------------------------------------------------
// Set up Express middlewares
// ------------------------------------------------------
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// ------------------------------------------------------
// Define routes and logic
// ------------------------------------------------------
app.post('/status', (req, res, next) => {
let payload = {username: req.body.username, status: req.body.status};
pusher.trigger('new_status', 'update', payload);
res.json({success: 200});
});
app.post('/online', (req, res, next) => {
let payload = {username: req.body.username};
pusher.trigger('new_status', 'online', payload);
res.json({success: 200});
});
app.get('/', (req, res) => {
res.json("It works!");
});
// ------------------------------------------------------
// Catch errors
// ------------------------------------------------------
app.use((req, res, next) => {
let err = new Error('Not Found: ');
err.status = 404;
next(err);
});
// ------------------------------------------------------
// Start application
// ------------------------------------------------------
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:
module.exports = {
appId: 'PUSHER_ID',
key: 'PUSHER_KEY',
secret: 'PUSHER_SECRET',
cluster: 'PUSHER_CLUSTER',
};
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:
{
"main": "index.js",
"dependencies": {
"body-parser": "^1.16.0",
"express": "^4.14.1",
"pusher": "^1.5.1"
}
}
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.
Testing our realtime status update application
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.
Conclusion
In the article, we have been able to create an iOS application with realtime user status updates, similar to what an application like WhatsApp currently has.
Have a question or feedback on the article? Please ask below in the comment section. The repository for the application and the Node backend is available on Github.
This post first appeared on the Pusher blog.