Let’s implement news feed for my Groser iOS app, it’s a front end to our same day grocery delivery service in Moscow. When shoppers pick order in the store they update product prices and create new products. This activity will generate events which our clients can view inside iOS app, discover what’s new and find out about price changes.
The idea is to prompt users to return to the app and act on news feed events by ordering groceries.
Create static feed
To start we’ll create Api::NewsFeedController in Rails app:
I prefer to build iOS UI first and work on returning real news feed events. So we return JSON of static news items and move on to start working on iOS app.
Let’s start wtih 2 basic kinds of events for now
New Product. Happens when shopper creates new product which was missing from our catalog
Price Change. So clients can bargain hunt without visiting store in person
Format date and currency on backend so that clients won’t have to do that
Building iOS feed consumer
Fire up Xcode and create new Swift class NewsFeedViewController inherit it from UICollectionViewController
Drag onto storyboard UICollectionViewController and set it’s class to NewsFeedViewController. We’ll show each event in news feed as a list of collection view cells.
Rough mockup of news feed with 3 item types
Model & Network Layer
Create GRNewsFeedEvent Objective C class to represent news feed events. We use Objective C for model classes because I use Mantle for JSON to models mapping and it isn’t compatible with Swift 2.0. I’ll prefer Swift for new code and use Objective C only when necessary.
Next up, add new method to network service GRGroserService to request all news feed events (it will be disastrous in production, we’ll add paging later)
Method buildParameters creates request parameters with device id, user access token for signed in user, timestamp, etc for logging.
Setup JSON to model mapping with Overcoat, it acts as a glue between Mantle and AFNetworking returning parsed models from REST requests.
Completion block passed to loadNewsFeedCompletion will receive NSArray of GRNewsFeedEvents.
Create new UICollectionViewCell descendant for each event type. Add initialization method to load data from model into view
Layout collection view cells in storyboard and setup subview outlets
We’ll use similar layout for New Product and Price Change cells
Format product name, price and size using NSAttributedString. Display price and size under product name, without using second label and setting up contraints. Also make a thin grey border around cell in awakeFromNib.
Display events in UICollectionView
Returning to our view controller. Implement UICollectionViewDataSource protocol and make cells as wide as our view in viewDidLoad.
Time to request news feed events from backend. Call following method from viewDidLoad.
Again, you don’t have to know everything before starting to work. In this case I didn’t know how to define weak self reference for closure with arguments in Swift (I’m still learning it), quick Google search and I found Stack Overflow answer.
Wire up NewsFeedViewController with TabBarController
Run the app
I declared events var as optional and when tried to unwrap it in collectionView:numberOfItemsInSection I got EXC_BAD_ACCESS exception. From my Objective C days I got used that nil is equivalent to 0, so if events is nil, call to count will be evaluated as nil and be casted to 0.
Swift is strongly typed language, so I had to initialize events var as empty array.
Yay, at least it doesn’t crash :)
Not what I expected, let’s see what is happening. To start I’ll add more pleasant background color to CollectionView, and make cells’ background white.
Next, It seems that server returns HTTP 500 error, because I use time_ago_in_words view helper inside NewsFeedController, update it to include ActionView::Helpers::DateHelper and ActionView::Helpers::NumberHelper for number_to_currency helper.
It’s nice to know that app is doing something, and actually works. Show UIActivityIndicator while events are being loaded. And here is the result.
Groser News Feed
Making it real
I realized that product’s price will keep changing after creation, and using current product’s price in news feed is not correct. We have to keep track new product’s price in news event.
I’ll start by creating NewsItem model in our rails app
Obviously each news item is related to Product so we reference it, everything else will be kept in Postgres json field. To keep up with Rails’s convention I’ll rename NewsFeedController to NewsItemsController.
Sometimes product has to be deleted, because it’s a duplicate, remove related news items as well.
Generating NewsItems for new products
To generate NewsItem for new products I’ll use Product’s after_create callback. But to keep Product model focused, I’ll use Publish/Subscribe mechanism. Since I already use Whisper gem in other parts of the project, include Wisper::Publisher into Product model and broadcast product_created event.
I can’t subscribe to product_created event on the Product instance before it’s created and subscribing after creation is too late. Thankfully Whisper has mechanism for temporary global observers, let’s use it.
Generating NewsItems for product price updates
In the same vein, let’s create NewsItems for product price updates. Update Product definition with after_update callback.
I use unit_price, and track store_price changes because unit_price is the calculated price shown to consumer, and store_price is what will change when shopper updates price while picking an order in store.
Make price_updated event more informative and extend it with change percentage and direction.
Update NewsItemsController to return NewsItems
Order NewsItems from most to least recent and render them as JSON using JBuilder. Since we use views to generate JSON, view helper includes also gone from the controller.
Seed with real news items
Here we are going to use treasure trove of historic data we’ve been tracking on server to create NewsItems. First step is to write rake task to generate NewsItems with new Products for the last 30 days.
To avoid code duplication extract specific NewItem creation from ProductUpdatesListener into NewItem class methods.
In the following snippet we select all products which where created in the last month for the only store we deliver from, and create NewsItems for each Product. Product always contains current price, however it may have been created with another price. We try to find WarehouseProductPrice for this product created on the same date which keeps history of product price updates.
WarehouseProductPrice keeps history of product price updates and NewsItem requires previous product price. We iterate over WarehouseProductPrices with sliding window of 2 elements. Since we ordered records from most recently created to least recently, first item in pair is the price after update and second is the price before update.
I pass created_at attribute when creating NewsItem, otherwise NewsItem.created_at will be current date. Final version of NewsItem:
Let’s see how it all works
First error I see, is that I used existing attribute name NewsItem.attributes, rename it to NewsItem.parameters and update NewsItem and JBuilder template. Now rake news_items:seed generates NewsItems from history.
Enhance news feed
I want to let users easily tell which CollectionView cell is new product and which is price update. Also I will show if price rose or declined and by how much, using text and color.
Create NewsItemsHelper view helper and format title of the NewsItem.
I also added two new convience methods to NewsItem. price_updated? needed for conditional JSON generation, and params allows access to NewsItem.parameters by symbol.
Quick check that everything works as expected. Instead of curl I use httpie that formats response nicely.
Refactor iOS app news items related code
After finishing with our server side let’s make naming consistent with our server side model, individual events become News Items and one CollectionView cell can present both NewsItem types.
Rename GRNewsFeedEvent to GRNewsItem, and GRNewsFeedEventType to GRNewsItemType.
Rename NewProductEventCell to NewsItemCell
Remove PriceChangeEventCell collection view cell and handle NewsItem title formatting in NewsItemCell
Rename NewsItems REST route from news_feed to news_items in our networking code
Change CollectionViewCell reuse identifier to NewsItemCell
I hit an exception NSUnknownKeyException reason: UICollectionViewCell setValue:forUndefinedKey: this class is not key value coding-compliant for the key imageView, probably because there was some reference in storyboard to removed 2nd collection view cell, hit Shift + Command + K to clean project, built again and exception has gone. After couple of finishing touches our News Feed looks like this.
News Feed after implementing all of the above steps
Let’s remove confusion around what News Feed is supposed to display when there are no news items or connection is offline and news items can’t be loaded. I’ll use DZNEmptyDataSet Cocoapod.
Implement DZNEmptyDataSetSource and DZNEmptyDataSetDelegate protocols. Below is the partial update of NewsFeedViewController with parts relevant to DZNEmptyDataSet protocols’ implementation. I added new boolean property to hide empty data set view while we are waiting for network response. And added button to allow user to manually reload news feed if it’s empty.
News feed empty state
Pull to refresh
Right now news feed loads one time when user opens the tab. For the impatient ones, who would like to refresh news feed at will we add Pull to Refresh control, the one where you pull down on scrollable view to refresh it’s contents. I stumbled on MAGearRefreshControl which I immediately liked.
I tried to install it using Cocoapods, but control requires minimum iOS 8, and I have to support iOS 7 for some time. I’ll just import source file into Xcode project.
Next, implement MAGearRefreshDelegate protocol and configure colors.
I also added boolean argument to loadEvents(showActivityIndicator: Bool) to disable activity indicator when update was triggered by refresh control.
News Feed Pull to Refresh
Show related product
It would be strange to see product price updates and won’t be able to act on it. Final piece, show ProductViewController when user taps news item.
Production News Feed
Show maximum 50 events for the last month.
I use Capistrano for code deployment, so I need to add deploy.rb action to run rake task on server.
Commit, deploy code to production and seed news items.
And here is the final result:
Production News Feed
As I expected there is a lot of noise, shoppers make mistakes and reverse price updates, or price deviates too little, there is no sense in displaying such noise.
I added filter to discard price updated news items if percetage change less than 3%: