Build a Twitter feed using Swift
A basic understanding of Swift and Node.js is needed to follow this tutorial.
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 Channels
Create a free account with Pusher.
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 frontend tech, and Node.js as your backend 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.
Further reading
8 March 2017
by Esteban Herrera