Presenting news feed in Real-time with Streamdata.io
Here is how to implement Server Sent Events for iOS. I personally like to read the news on my Mobile when I have some time on my hands, like in the metro or while waiting for a doctor’s appointment. There are a lot of news applications out there, but they all have something in common: the lack of Real-time updates.
Most of them will propose the classic pull-to-refresh, so basically if you want your news update, well you’d better use that finger! Some favor a regular polling for updates, and usually you can’t miss that insidious counter in the corner, constantly going from 10 to 0 until the activity indicator shows up. Oh, suspense is killing me! And 9 times out of 10, we waited for nothing, as there really is nothing new yet. How frustrating.
Well, enough of that now! In this article, I’ll propose an innovative way to get your fresh news sent straight to you while you’re reading the latest blog post on your favorite topic, so it’s there when you come back for more.
The news feed API
For the purpose of this article, and because it’s usually a popular choice, I’ve based my development on the Times Newswire API from the New York Times. You can go to the NYT Developers page and just create a free account to get your API key. Or, instead, you could use your favorite news provider, as long as it exposes a news feed API. It would also require a small interfacing work.
The NYT API provides the data in a JSON format, with 3 main fields:
– A status field usually containing the text “OK” (to be honest, I’ve never seen another status than OK during all the time I have worked with this API).
– An ID field “num_results” containing a unique auto-incremented integer for each news feed.
– An array of news “results” containing the 20 latest news items in the feed.
Building the project
To keep things simple, our project is based on the Single View Application template, and the storyboard is limited to 3 cascading view controllers linked by segues:
– The Feed View Controller holding the main TableView displaying the news feed,
– The Details View Controller displaying more details on a selected news item,
– The Web View Controller presenting the Web page of the selected news from the NYT website.
In the news TableView, we will use custom cells presenting for each news item its news section, its title, and a thumbnail image (if existing).
Ok, now that we have our application shell, let’s put some content into it!
Streaming the data
A first idea would be to poll our API, get the JSON response, parse it to create an array of news items, and then use this array to populate our TableView. Very simple indeed, but remember what we said in the introduction: we want to provide the user with Real-time updates of the news feed. In other words, instead of polling the API to get the data, we want the API to stream the data to our application when updates are available.
Thanks to Streamdata.io, we now have an easy, yet efficient way of doing exactly this. So how does it work? Streamdata.io acts as a proxy between the API and your application. Instead of calling the API, the app will call the proxy, and it is the proxy that will poll the API at a defined frequency and send the data to the application when it has changed.
The call creates a persistent Server-Sent Events (aka SSE) connection between the app and the proxy that will be used to push the updates. But wait! There is more! When the proxy polls the API for the first time, it pushes the full JSON data to the application, but after that it will only push incremental updates as JSON patches, reducing drastically the amount of data transferred to your app.
See in the diagram below the difference in data usage for our news feed API when called with and without Streamdata.io, with a 5s polling frequency after only 45 seconds! In this example, we first received the full JSON data representing about 30kb, then every 5s a small patch for minor changes in the data (if you look closely you can see the green line going up a bit). Compared to the constant 30k-size flow of data received every time you poll your API, you can see the tremendous saving you will make in your device’s bandwidth.
You don’t believe me? Then try streaming your favorite API now and see for yourself!
Now to use this in our application, we just have to create a free account on Streamdata.io and get our app token, as simple as that. Instead of using our API URL, we will call this specific Streamdata.io URL using the app token:
https://streamdata.motwin.net/http://api.nytimes.com/svc/news/v3/content/all/all.json?api-key=YOUR_NYT_API_KEY&X-Sd-Token=YOUR_STREAMDATA_APP_TOKEN
Event Handling
To enable the proxy server to push data to our application, and also not reinvent the wheel, we will use a dedicated library called TRVSEventSource (created by Travis Jeffery, here is the Github link). This library provides an API for opening an HTTP connection for receiving push notifications from a server in the form of Server-Sent Events (SSE).
// Initialize the TRVSEventSource event source with URL string
URL = [NSURL URLWithString:NEWSFEED_URL];
eventSource = [[TRVSEventSource alloc] initWithURL:URL];
eventSource.delegate = self;
// Open the event source
[eventSource open];
In order to use the connection in the application, we start by initializing in the viewDidLoad method of the Feed View Controller an event source instance with our target URL – in this case the above Streamdata.io URL – and setting the event source delegate to our controller class (of course this means that we have to declare the delegate in our class interface). Then all we need to do is open the event source.
Now we have an open SSE connection between the proxy server and our device, able to stream data to the application. The next step is to react when new data is pushed. That’s where the delegate we declared earlier comes in handy: the TRVSEventSource library has a specific method to handle the reception of a new event, that we can implement in our Feed View Controller in order to use this data:
- (void)eventSource:(TRVSEventSource *)eventSource didReceiveEvent:(TRVSServerSentEvent *)anEvent;
First, let’s read the JSON data received by the application and store it in a specific collection object (I called it dataObject to avoid confusion).
newResult = NO;
NSError *e;
if([anEvent.event isEqualToString:@"data"]==TRUE) // if event of type "data"
{
// Reading data from JSON
dataObject = [NSJSONSerialization JSONObjectWithData:anEvent.data options:NSJSONReadingMutableContainers error:&e];
resultId = [dataObject objectForKey:@"num_results"];
newResult = YES;
}
else if ([anEvent.event isEqualToString:@"patch"]==TRUE) // if event of type "patch"
{
// Reading data from JSON
NSArray *patch =[NSJSONSerialization JSONObjectWithData:anEvent.data options:NSJSONReadingMutableContainers error:&e];
// Applying patch to data
[JSONPatch applyPatches:patch toCollection:dataObject];
resultId = [dataObject objectForKey:@"num_results"];
if (resultId > currentResultId)
{ newResult = YES; }
}
Remember that after opening the event source, the first event sent by Streamdata.io is the full JSON data (of type “data”) that we convert to a collection object and store in our dataObject, using the Apple NSJSONSerialization class. All other events are JSON patches (of event type “patch”) that we first convert to a patch collection, which is then applied to the dataObject. This means that the dataObject will always have the up-to-date JSON information received from the proxy server. For this last operation, we use a dedicated library called JSONTools (created by Gregory Combs, here is the Github link), which I will not detail in this article.
During my development, I noticed that most probably due to some caching issues on the NYT API side, successive patches would sometimes give me older data with a preceding num_result, potentially resulting in the display of an older news feed. I simply solved this problem by updating the news feed displayed in my TableView only when the newly updated dataObject had a num_result greater than the one of the currently displayed news feed. This is reflected by the newResult Boolean set to YES when an update of the news feed is required.
// if a new result has arrived that needs updating the table
if (newResult)
{
dataResults = [dataObject objectForKey:@"results"];
currentResultId = resultId;
currentFeed = [[NSMutableArray alloc] init];
for (NSDictionary *news in dataResults)
{
NewsItem *newsItem = [[NewsItem alloc] initWithJSONDictionary:news];
[currentFeed addObject:newsItem];
}
// Refresh the content of both TableViews with new result
NSLog(@"Refresh TableViews");
[self.feedTableView performSelectorOnMainThread:@selector(reloadData)
withObject:nil waitUntilDone:YES];
}
When we have to update the news feed, we empty the current feed array, we parse the results array contained in the dataObject, and we populate the current feed array (called currentFeed) with the corresponding news items. Finally we reload the data of the feed TableView. Usually you would just call the reloadData method of the TableView delegate for this, but if we did here, it would be called in the background event source thread, hence possibly (and by experience pretty much every time) with a significant delay. The method performSelectorOnMainThread enables to invoke reloadData on the main thread so it is executed right away.
The news item object class
// NewsItem.h
@interface NewsItem : NSObject
@property (readonly) NSString *section;
@property (readonly) NSString *subsection;
@property (readonly) NSString *title;
@property (readonly) NSString *abstract;
@property (readonly) NSString *url;
@property (readonly) NSString *byline;
@property (readonly) NSString *thumbnail_standard;
(…)
@property (readonly) NSArray *multimedia;
@property (readonly) NSArray *related_urls;
- (id)initWithJSONDictionary:(NSDictionary *)jsonDictionary;
@end
In order to facilitate the integration of a different news feed, I created a specific newsItem class. Its properties correspond to the fields of the news items in the results array, and it contains one method to initialize the object with a JSON dictionary (in our case the news item).
This class will act as an interface between the API and our application, so we potentially could only have to adapt the initialization method in order to use our application with another news feed API providing a similar JSON response.
It’s these class objects that are created and inserted in the currentFeed array. It’s also these objects that will be used to display the data in the feed TableView cells and passed to the descending views.
Refreshing the feed TableView
Once we have called the reloadData method, we need to implement the method tableView:cellForRowAtIndexPath: from our feed TableView data source, because that’s where the magic happens!
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NewsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"tableViewCell" forIndexPath:indexPath];
// Get specific news item to display in cell
NewsItem *newsItem = [currentFeed objectAtIndex:indexPath.row];
// Set news section label text
cell.newsSection.text = newsItem.section;
(…)
First, we initialize the custom cell and we get the corresponding news item from the currentFeed array. Then we set the text of the section label.
(…)
// Set the default thumbnail image
cell.newsImage.image = [UIImage imageNamed:@"noImage.png"];
// If the news item has a thumbnail image URL, asynchronously load and display the image
if (newsItem.thumbnail_standard.length > 0)
{
dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:newsItem.thumbnail_standard]];
if (imgData)
{
UIImage *image = [UIImage imageWithData:imgData];
if (image)
{
dispatch_async(dispatch_get_main_queue(), ^{
NewsTableViewCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
if (updateCell) { updateCell.newsImage.image = image; }
});
}
}
});
}
(…)
Then we set the thumbnail image to use our default image, and if the news item has a thumbnail image URL, we asynchronously load it and replace the default one.
(…)
// Get initial news title label frame
CGRect initLabelFrame = cell.newsTitle.frame;
// Set news title label text & type
cell.newsTitle.lineBreakMode = NSLineBreakByWordWrapping;
cell.newsTitle.numberOfLines = 0;
cell.newsTitle.text = newsItem.title;
// Adapt news title label frame to text
CGRect labelRect = [cell.newsTitle.text boundingRectWithSize:CGSizeMake(initLabelFrame.size.width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:cell.newsTitle.font} context:nil];
cell.newsTitle.frame = CGRectMake(initLabelFrame.origin.x, initLabelFrame.origin.y, initLabelFrame.size.width, fmax(initLabelFrame.size.height, labelRect.size.height));
// Save title height to adapt cell height in method "heightForRowAtIndexPath"
titleHeight = cell.newsTitle.frame.size.height;
return cell;
}
Finally, we set the text of the title label before returning the populated cell. As the label has a fixed size and we initially don’t know the length of the title, we have to adapt the height of the label and the height of the cell to the text.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{ return 1; }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{ return [dataResults count]; }
Let’s not forget to implement the remaining data source methods of the feed TableView to set the number of sections in the TableView and the number of rows in section.
Accessing the news details view
The final part in this article describes how to access the corresponding news details view when selecting a specific row in the feed TableView. For this we have to implement the TableView delegate method tableView:didSelectRowAtIndexPath: that is called when the user clicks on a TableView cell.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Invoke the segue to open the news details view
[self performSegueWithIdentifier:@"ShowDetails" sender:self];
}
Here we just tell our current view to look for the segue labeled ShowDetails (the identifier of the segue is a property of that segue defined in the storyboard) and invoke that segue’s action, which starts by calling the method prepareForSegue:sender: to perform additional logic and/or pass relevant data to the new view controller.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"ShowDetails"])
{
// Get specific news item from selected cell
NewsItem *newsItem = [currentFeed objectAtIndex:[self.feedTableView indexPathForSelectedRow].row];
// Pass selected news item to destination view controller
DetailsViewController *destViewController = segue.destinationViewController;
destViewController.newsItem = newsItem;
}
}
In our case we just want to provide our details view controller with the news item, corresponding to the row clicked by the user, it will then use to display the necessary data on the new page presented. So, if the segue’s identifier is ShowDetails, first we need to identify the news item selected in the currentFeed array using the index path of the selected cell. Then all we have to do is pass this news item to the destination details view controller.
Now when clicking on a cell in our feed TableView, the details page will be presented modally (another property of the segue defined in the storyboard) displaying more details about the news item.
Conclusion
In this article, we have learned how to use the Streamdata.io proxy with an event source to stream the content of a frequently updated API, and display this data in a TabionalleView which content will be automatically refreshed every time a new data is received.
In order to keep things simple, a lot of topics have been purposefully ignored. Some of these topics will be treated in a further blog post, like the caching capabilities of the Streamdata.io proxy, handling the event source connection during application state transitions (entering background/foreground mode, terminating the app), or error & exception management in the application.
You can download the full source code for your reference on GitHub here.
Download the whitepaper on navigating the new streaming API landscape.
That’s all for now!
Follow us on social