This blog post was written under the Pusher Guest Writer program.

Every second, thousands of tweets are tweeted on Twitter. To make sense of it all, we can track what interests us by following tweets with certain hashtags. In this tutorial, we’re going to build an iOS realtime Twitter feed app with Pusher to track a custom set of hashtags.

In the backend, we’ll have a Node.js process listening for tweets that contain one or more defined hashtags. When we find one, we’ll publish an object with the tweet information to a Pusher channel.

On the other hand, the iOS app will be listening for new tweets in that channel to add them to a feed. To keep things simple, the app will only show some basic information for each tweet. This is how the app looks like in action:

The complete source code of the Node.js process and the iOS app is on Github for reference.

Let’s get started!

Setting up Pusher

Create a free account at https://pusher.com/signup.

When you first log in, you’ll be asked to enter some configuration options to create you app:

Enter a name, choose iOS as your front-end tech, and Node.js as your back-end tech. This will give you some sample code to get you started:

But don’t worry, this won’t lock you into this specific set of technologies, you can always change them. With Pusher, you can use any combination of libraries.

Then go to the App Keys tab to copy your App ID, Key, and Secret credentials, we’ll need them later.

Setting up a Twitter app

Log in to your Twitter account and go to https:/apps.twitter.com/app/new to create a new application.

You’ll have to enter the following:

  • Name (your application name, which shouldn’t have to be taken, for instance, pusher_hashtags_1)
  • Description (your application description)
  • Website (your application’s publicly accessible home page. We’re not going to use it, so you can enter http://127.0.0.1).

After you agree to Twitter’s developer agreement, your application will be created.

Now go to the Keys and Access Token section and create your access token by clicking on the Create my access token button:

Save your consumer key, consumer secret, access token, and access token secret since we’ll need them later.

The backend

We’ll get the tweets by using the Twitter Streaming API with the help of the twit library.

You can clone the GitHub repository and run npm install to set up dependencies.

Then, create a config.js file from config.sample.js :

cp config.js.sample config.js

And enter your Twitter and Pusher information:

module.exports = {
  twitter: {
    consumer_key        :  '<INSERT_TWITTER_CONSUMER_KEY_HERE>',
    consumer_secret     :  '<INSERT_TWITTER_CONSUMER_SECRET_HERE>',
    access_token        :  '<INSERT_TWITTER_ACCESS_TOKEN_HERE>',
    access_token_secret :  '<INSERT_TWITTER_ACCESS_TOKEN_SECRET_HERE>',
  },

  pusher: {
    appId      : '<INSERT_PUSHER_APP_ID_HERE>',  
    key        : '<INSERT_PUSHER_KEY_ID_HERE>',
    secret     : '<INSERT_PUSHER_SECRET_ID_HERE>',
    encrypted  : true,
  },

  hashtagsToTrack: ['#nodejs', '#swift', '#ios', 'programming'],

  channel: 'hashtags',

  event: 'new_tweet',
}

You can also change the hashtags to track if you want.

Our main file, app.js, is simple. After some setup code, we configure the Twitter stream to filter tweets by the hashtags to track:

const config = require('./config');
const Twit = require('twit');
const Pusher = require('pusher');

const T = new Twit(config.twitter);

const pusher = new Pusher(config.pusher);

const stream = T.stream('statuses/filter', { track: config.hashtagsToTrack });

When a new tweet matches our conditions, we’ll extract the properties our iOS client will need to create an object and publish it to a Pusher channel as a new_tweet event:

...

stream.on('tweet', (tweet) => {
  const message = {
    message: tweet.text, 
    username: tweet.user.screen_name, 
    name: tweet.user.name, 
  };

  pusher.trigger(config.channel, config.event, message);
});

And that’s all this process does, let’s see how the iOS client is built.

Setting up the Xcode project

We’ll be using iOS 10 and Swift 3 to build our app, so Xcode 8 is required.

To start, open Xcode and create a Single View Application:

Give it a name, choose Swift as the language and Universal in the Devices option:

Now, we’re going to install the project dependencies with CocoaPods. Close your Xcode project, and in a terminal window go to the top-level directory of your project and execute this command:

pod init

This will create a text file named Podfile with some defaults, open it and add as dependencies PusherSwift and AlamofireImage. It should look like this:

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

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

  # Pods for twitter_feed_pusher
  pod 'PusherSwift'
  pod 'AlamofireImage'

end

Now you can install the dependencies in your project with:

pod install

And from now on, make sure to open the generated Xcode workspace instead of the project file:

open twitter_feed_pusher.xcworkspace

For apps like this, creating everything programmatically is easier. We won’t use the Interface Builder or the storyboard file that Xcode creates (Main.storyboard).

So let’s start by opening the file AppDelegate.swift to manually create the window in which our app is going to live and specify a rootViewController:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: 
        [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        // Override point for customization after application launch.
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.makeKeyAndVisible()

        window?.rootViewController = UINavigationController(rootViewController: ViewController())

        return true
    }

    ...
}

In the above code, we’re using the ViewController file Xcode created for us.

Now let’s use a UITableViewController, this will give us for free an UITableViewDataSource and an UITableViewDelegate.

Open the ViewController class and make it extend from UITableViewController:

import UIKit

class ViewController: UITableViewController {
    ...
}

If you run the app at this point, you should see something like the following:

Now let’s take a closer look at how the app is going to present a tweet. It has a profile image, the name of the user, the Twitter username, and the text of the tweet:

So let’s create a new Swift file, Tweet.swift, to create a structure that will hold the tweet’s information:

import Foundation

struct Tweet {
    let name: String
    let username: String
    let message: String
}

We don’t have to create a property to hold the profile image URL, we can get it from the username.

We’re going to need a custom cell class for our UITableView. Once again, create a new Swift file, this time with the name TweetCell.swift and the following content:

import UIKit

class TweetCell: UITableViewCell {

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

To simplify the layout, our cell is going to have a UIImageView for the image profile and a UITextView for the rest of the content, which will be formatted as an attributed string:

So let’s add these views along with some properties:

class TweetCell: UITableViewCell {

    let profileImage: UIImageView = {
        let imageView = UIImageView()
        imageView.layer.cornerRadius = 5
        imageView.clipsToBounds = true
        imageView.translatesAutoresizingMaskIntoConstraints = false

        return imageView
    }()

    let messageText: UITextView = {
        let textView = UITextView()
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.isUserInteractionEnabled = false

        return textView
    }()

    ...
}

It’s important to set translatesAutoresizingMaskIntoConstraints to false because we’re going to use the NSLayoutAnchor API to position our views.

Let’s add the UIImageView at the top-left corner of the cell, with an offset of 12 points on both left and top, and a width and height of 50:



class TweetCell: UITableViewCell { ... override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) addSubview(profileImage) profileImage.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true profileImage.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 12).isActive = true profileImage.widthAnchor.constraint(equalToConstant: 50).isActive = true profileImage.heightAnchor.constraint(equalToConstant: 50).isActive = true } ... }

And the UITextView at the left of the profile image, with a top and left offset of 4 points and using all the available space of the cell:

class TweetCell: UITableViewCell {
    ...

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        ...

        addSubview(messageText)

        messageText.topAnchor.constraint(equalTo: self.topAnchor, constant: 4).isActive = true
        messageText.leftAnchor.constraint(equalTo: profileImage.rightAnchor, constant: 4).isActive = true
        messageText.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        messageText.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
    }

    ...
}

Now let’s add a property observer so when an object of type Tweet is set on this cell, we can compose our attributed string and set the value of the profile image (don’t forget to import AlamofireImage):

import UIKit
import AlamofireImage

class TweetCell: UITableViewCell {

    var tweet: Any? {
        didSet {
            guard let t = tweet as? Tweet else { return }

            // Add the name in bold
            let attributedText = NSMutableAttributedString(string: t.name, attributes: [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 16)])

            // Add the Twitter username in grey color (and a new line)
            attributedText.append(NSAttributedString(string: " @\(t.username)\n", attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14), NSForegroundColorAttributeName: UIColor.gray]))

            // Modify the line spacing of the previous line so they look a litle separated
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineSpacing = 5
            let range = NSMakeRange(0, attributedText.string.characters.count)
            attributedText.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: range)

            // Add the message
            attributedText.append(NSAttributedString(string: t.message, attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14)]))

            messageText.attributedText = attributedText

            // Compose the image URL with the username and set it with AlamofireImage
            let imageUrl = URL(string: "https://twitter.com/" + t.username + "/profile_image")
            profileImage.af_setImage(withURL: imageUrl!)
        }
    }

    ...
}

Now, in the ViewController class, let’s create a cell identifier, an array that will control our tweets and the Pusher object:

import UIKit
import PusherSwift

class ViewController: UITableViewController {

    let cellId = "cellId"
    var tweets = [Tweet]()
    var pusher: Pusher! = nil

    ...
}

So we can register the type TweetCell and instantiate the Pusher object inside viewDidLoad:

class ViewController: UITableViewController {

    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Pusher Feed"

        tableView.register(TweetCell.self, forCellReuseIdentifier: cellId)

        pusher = Pusher(
            key: "<INSERT_YOUR_PUSHER_KEY_HERE>"
        )


    }
}

We will listen to new tweets by subscribing to the channel hashtags and binding the event new_tweet :

class ViewController: UITableViewController {

    ...

    override func viewDidLoad() {

        ...

    let channel = pusher.subscribe("hashtags")

        let _ = channel.bind(eventName: "new_tweet", callback: { (data: Any?) -> Void in
            if let data = data as? [String : AnyObject] {

                // Extract the Tweet information
                let message = data["message"] as! String
                let name = data["name"] as! String
                let username = data["username"] as! String

                // Create a tweet
                let tweet = Tweet(name: name, username: username, message: message)

                // Insert it at the beginning of the array
                self.tweets.insert(tweet, at: self.tweets.startIndex)

                // Insert the new tweet at the beginning of the table and scroll to that position
                let indexPath = IndexPath(row: 0, section: 0)
                self.tableView.insertRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
                self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.none, animated: true)
            }
        })

        pusher.connect()
    }
}

This way, we can extract the tweet information, create a Tweet instance and insert it in the array and in the tableView to display it.

Of course, for this to happen, we also need to implement the following methods:

class ViewController: UITableViewController {

    ...

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tweets.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! TweetCell

        // At this point, the didSet block will set up the cell's views
        cell.tweet = tweets[indexPath.item]

        return cell;
    }
}

One last detail. The cells need to have a dynamic height to accommodate the text of the tweet. One easy way to achieve this is by disabling the scrolling on the cell’s UITextView:

class TweetCell: UITableViewCell  {

    ...

    let messageText: UITextView = {
        let textView = UITextView()
        ...

        textView.isScrollEnabled = false

        return textView
    }()

    ...
}

Estimating an average row height, and setting the rowHeight property this way:

class ViewController: UITableViewController {

    ...

    override func viewDidLoad() {
        ...

        tableView.estimatedRowHeight = 100.0
        tableView.rowHeight = UITableViewAutomaticDimension

        ...
    }

    ...
}

And we’re done!

Testing the app

Run the iOS app in the simulator or in a real device:

And execute the backend with:

node app.js

Or if you only want to test the app, you can use the Pusher Debug Console on your dashboard:

When a new_tweet event is received in the Pusher channel, the new tweet will come up in the iOS app:

Conclusion

You can find the final version of the backend here and the final version of the iOS app here.

Hopefully, this tutorial has shown you how to build a Twitter feed for an iOS app with Pusher in an easy way. You can improve the app by showing more information or saving it to a database. Remember that your forever free Pusher account includes 100 connections, unlimited channels, 200k daily messages, SSL protection, and there are more features than just Pub/Sub Messaging. Sign up here.

Further reading