Codementor Events

Build a realtime counter for iOS using Pusher

Published Feb 16, 2018

One of the most common elements on applications (web or otherwise) is counters. YouTube, for instance, uses counters to see how many people have viewed a particular video. Facebook also does the same for videos on their platform.

Most of the counters on these sites, however, only update the count when you have refreshed the page. This leaves a lot to be desired, as sometimes you just want to see the number increase in realtime. This gives you the impression that the item is being viewed by many people at the moment.

In this article, we are going to explore how we can leverage the realtime nature of Pusher to create a counter that updates in realtime. We will be creating a video viewer iOS application with a realtime counter showing how many people have viewed the video.

To follow along, you will need basic knowledge of Swift, Xcode and command line. You will also need to set up a Pusher account, and create an application. You can do so here.

Creating our realtime counter app using Pusher

To get started you will need Cocoapods installed on your machine. Cocoapods is a package manager and we will be using this to manage the dependencies on the application. To install Cocoapods, type this in your command line:

$ gem install cocoapods

After you are done installing that, launch Xcode and create a new single page application project. Follow the set up wizard, and then once the Xcode project editor is open, close Xcode. cd to the root directory of your project and run the command:

$ pod init

This should create a Podfile in the root of your project. Open this file in your favorite editor and then update the contents of the file to the following:

# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'

target 'counter' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for counter
  pod 'Alamofire'
  pod 'PusherSwift'

end

In the file above we have specified two dependencies: PusherSwift and Alamofire. These will be useful later in the project. Now install these dependencies by running this command from your terminal:

$ pod install

Once this is complete, you should have a .xcworkspace file in the root directory of your project. Open this file and it should launch Xcode. (Make sure you don’t have any instance of Xcode running for this project before opening the file or you will get an error.)

Creating the views for our realtime application

Now that the project is open, we will create some views for our application. Open the Main.storyboard file and in there we will create the views.

We want to create a navigation controller that will have a ViewController as the root controller of the navigation controller. Then in the new view controller, we will add a webview; this is where we will be embedding the video we want people to view. We will also add two labels, one for the counter and the other will just be a plain immutable message.

After we are done, this is what we have so far:

Adding our realtime feature to our application

Now that we have created the application, we can now add the code that will interact with the views and add the realtime support and the video also.

Create a new MainViewController class and link it to the view controller we created above. Then create a split view in Xcode and ctrl+drag from the webview to the view controller. This should create an @IBOutlet in the controller; do the same for the counter label so it creates another @IBOutlet. Our controller should now have two @IBOutlets one for the webview and one for the counter label. Great.

Now we are going to add the code to load our video. We are going to be using a YouTube video of a Pusher tutorial for this exercise. In the viewDidLoad method add the following:

override func viewDidLoad() {
    super.viewDidLoad()
    loadYoutube(videoID:"xDQ8vzD0lzw")
}

Now lets create the loadYoutube method and the other dependent methods:

func loadYoutube(videoID:String) {
    self.automaticallyAdjustsScrollViewInsets = false
    webview.allowsInlineMediaPlayback = true
    webview.mediaPlaybackRequiresUserAction = false
    let embedHTML = getEmbedHTML(id:videoID);

    let url: NSURL = NSURL(string: "https://www.youtube.com/embed/\(videoID)")!
    webview.loadHTMLString(embedHTML as String, baseURL:url as URL )
}

private func getEmbedHTML(id: String) -> String {
    return "<html><head><style type=\"text/css\">body {background-color: transparent;color: white;}</style></head><body style=\"margin:0\"> <iframe webkit-playsinline width=\"100%\" height=\"100%\" src=\"https://www.youtube.com/embed/\(id)?feature=player_detailpage&playsinline=1\" frameborder=\"0\"></iframe>";
}

Now we have instructed the application to load a YouTube video automatically. However, the counter functionality does not yet work. Let’s fix that.

Import the PusherSwift library and add a new method to update the counter using Pusher:

func updateViewCount() {
    let options = PusherClientOptions(
        host: .cluster("PUSHER_CLUSTER")
    )

    pusher = Pusher(key: "PUSHER_KEY", options: options)

    let channel = pusher.subscribe("counter")
    let _ = channel.bind(eventName: "new_user", callback: { (data: Any?) -> Void in
        if let data = data as? [String: AnyObject] {
            let viewCount = data["count"] as! NSNumber
            self.count.text = "\(viewCount)" as String!
        }
    })
    pusher.connect()
}

Note: Where it says PUSHER_CLUSTER and PUSHER_KEY, you should replace with your actual Pusher cluster and key. You’ll also need to import

Now you can just call the updateViewCount from the viewDidLoad method so it is called when the view is loaded.

One final thing we will do is use Alamofire to send a request to a backend so the counter can be updated and saved, so we do not lose the count of people who have viewed the video. Import Alamofire and add the following:

func sendViewCount() {
    Alamofire.request(endpoint, method: .post).validate().responseJSON { response in
        switch response.result {
        case .success:
            if let result = response.result.value {
                let data = result as! NSDictionary
                let viewCount = data["count"] as! NSNumber
                self.count.text = "\(viewCount)" as String!
            }
        case .failure(let error):
            print(error)
        }
    }
}

Now that we are done with that, theMainViewController should now look a little like this:

import UIKit
import Alamofire
import PusherSwift

class MainViewController: UIViewController {

    @IBOutlet weak var count: UILabel!
    @IBOutlet weak var webview: UIWebView!

    var endpoint: String = "http://localhost:4000/update_counter"

    var pusher : Pusher!

    override func viewDidLoad() {
        super.viewDidLoad()
        loadYoutube(videoID:"xDQ8vzD0lzw")
        sendViewCount()
        updateViewCount()
    }

    func sendViewCount() {
        Alamofire.request(endpoint, method: .post).validate().responseJSON { response in
            switch response.result {

            case .success:
                if let result = response.result.value {
                    let data = result as! NSDictionary
                    let viewCount = data["count"] as! NSNumber
                    self.count.text = "\(viewCount)" as String!
                }
            case .failure(let error):
                print(error)
            }
        }
    }

    func updateViewCount() {
        let options = PusherClientOptions(
            host: .cluster("PUSHER_CLUSTER")
        )

        pusher = Pusher(key: "PUSHER_KEY", options: options)

        let channel = pusher.subscribe("counter")
        let _ = channel.bind(eventName: "new_user", callback: { (data: Any?) -> Void in
            if let data = data as? [String: AnyObject] {
                let viewCount = data["count"] as! NSNumber
                self.count.text = "\(viewCount)" as String!
            }
        })
        pusher.connect()
    }

    func loadYoutube(videoID:String) {
        self.automaticallyAdjustsScrollViewInsets = false
        webview.allowsInlineMediaPlayback = true
        webview.mediaPlaybackRequiresUserAction = false
        let embedHTML = getEmbedHTML(id:videoID);

        let url: NSURL = NSURL(string: "https://www.youtube.com/embed/\(videoID)")!
        webview.loadHTMLString(embedHTML as String, baseURL:url as URL )
    }

    private func getEmbedHTML(id: String) -> String {
        return "<html><head><style type=\"text/css\">body {background-color: transparent;color: white;}</style></head><body style=\"margin:0\"> <iframe webkit-playsinline width=\"100%\" height=\"100%\" src=\"https://www.youtube.com/embed/\(id)?feature=player_detailpage&playsinline=1\" frameborder=\"0\"></iframe>";
    }
}

If we load the application now, it would load the video but the counter will not work. This is because we have not yet set up a backend logic.

Creating the backend for our realtime counter iOS application

For the backend, we will be creating a very basic NodeJS application. This application will simply have one endpoint that saves the counter state and sends a trigger to Pusher so other listeners subscribed to the channel event can pick it up and update in realtime.

To start, create a new directory for your application. In the application create two files:

File: package.json
{
  "main": "index.js",
  "dependencies": {
    "body-parser": "^1.16.0",
    "express": "^4.14.1",
    "pusher": "^1.5.1"
  }
}

File: index.js
var Pusher = require('pusher');
let express = require('express');
let bodyParser = require('body-parser');
let fs = require('fs');

let app = express();

let pusher = new Pusher({
  appId: 'PUSHER_ID',
  key: 'PUSHER_KEY',
  secret: 'PUSHER_SECRET',
  cluster: 'PUSHER_CLUSTER',
  encrypted: true
});

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/update_counter', function(req, res) {
  let counterFile = './count.txt';

  fs.readFile(counterFile, 'utf-8', function(err, count) {
    count = parseInt(count) + 1;
    fs.writeFile(counterFile, count, function (err) {
      pusher.trigger('counter', 'new_user', {count:count});
      res.json({count:count});
    });
  });
});

app.use(function(req, res, next) {
    let err = new Error('Not Found');
    err.status = 404;
    next(err);
});

module.exports = app;

app.listen(4000, function(){
  console.log('App listening on port 4000!')
})

Finally, create the counter.txt file in the same directory and chmod it to be writable.

$ echo "0" > count.txt
$ chmod 0644 count.txt 

Now run npm install to install all the dependencies that node needs to make the backend application work. When the dependencies are done installing, run node index.js to start your application.

One last change we would need to make to allow our application interact with the localhost application is in our projects info.plist file. Make the changes below before launching the application:

Now, when you launch the application you can see the counter increase, and if there is another instance of the application, they can see the counter increase in realtime. Neat!

Conclusion

In this article, we explored how you can create realtime counters using Pusher in your iOS applications. We are curious to see how you would incorporate this into your iOS application. If you have any other use cases you would like to see, or have any feedback, leave a comment below.

The source code to the application above is available on GitHub here, you can download it and experiment with the code to see what else is possible.

This post first appeared on the Pusher blog.

Discover and read more posts from Neo
get started