Dave Kasper & Chen Chen

(u/egonkasper & u/chenchencs)

A little over a year ago, Reddit released its first mobile app for iOS. In this article, we will discuss how we built the feed by starting with the building blocks of MVC, breaking out the networking code, creating powerful abstractions for handling paginated feeds, creating the building blocks of the UI, and optimizing to achieve high-performance scrolling. Using these techniques we were able to build a feed which had a very low crash rate (99.95% crash-free sessions) with an architecture that is still being used and extended today.

Primarily, the iOS app was designed to be a way to visualize and interact with Reddit content, including most importantly the feed of posts that make up the front page and subreddits. Unlike on the web, in the app the front page is displayed as an infinitely scrolling feed of posts. We sought to keep the app lightweight and rely on the server as the source of truth, maintaining simple caching only when needed for performance reasons.

We wanted to build a general purpose feed that could support high-performance scrolling and be extensible for the future. In order for the feed to serve as a foundation, we needed to build isolated components that could be reused throughout the app. However, we also needed to build many Reddit-specific features to support multiple types of posts from text and links to photos and gifs. Additionally, we needed to consider other customization options like night mode and compact mode.

Starting With Simple MVC

With an open-ended problem like how to build a feed, there are many architectures to consider, but on iOS the most common pattern is MVC, model-view-controller. So, perhaps the simplest approach to building a feed would be to create a view controller called FeedViewController (controller) which fetches Post objects (model) and displays them with a UICollectionView (view).

The problem is that you quickly end up with a different sort of MVC: the Massive View Controller, where the size of the controller’s code gets out of control since it contains too much logic. Also, if we want to try to reuse logic from the feed in other places it would be tough to do. More philosophically, we want to adhere to the principle of separation of concerns, meaning that each part of the system should be responsible for a single task, and here the controller is actually responsible for fetching, refreshing, caching, and displaying everything.

To solve this, we will describe how we separated the networking code into a class called NetworkRequestManager and the pagination logic into an abstraction called ListingNetworkSource . The ListingNetworkSource also handles the interaction with the model and store layers completing decoupling the controller from needing to work with them directly, a topic to discuss in depth in a future post. Finally, we will discuss how we built a flat hierarchy of view classes to create a feed which was simple and easily extensible.

Separating the Network From the Controller

The first chunk to break out of the FeedViewController is the networking code. We created a class called NetworkRequestManager which makes authenticated requests to the API through OAuth, and deserializes the JSON data. The networking manager can also handle things like prioritizing traffic or monitoring bad connectivity situations. This is a good start, but we can go further.

At Reddit, it turns out that most of our API endpoints are lists. We have lists of posts, lists of subreddits, lists of comments, and more. In its JSON format, a listing contains an array of objects, and keys called “after” and “before” which allow you to get the next or previous batch of objects. We encapsulated all the functionality of fetching into something called ListingNetworkSource . It has methods to fetch data, fetch more data (with the after key), and methods which subclasses use to parse the data into posts or other objects. For example, we could create a subclass FeedListingNetworkSource to handle parsing the post objects of the main feed and different subclasses to handle other situations like comments or live threads.

The super simplified version looks like this:

@interface ListingNetworkSource : NSObject

@property (nonatomic, weak, readwrite) NSObject *delegate;

@property (nonatomic, copy, readwrite) NSArray *objects;

@property (nonatomic, strong, readwrite) NSString *afterID;

- (void)fetchData;

- (void)fetchMoreData;

// for subclasses to override

- (void)parseData:(id)data;

- (void)parseMoreData:(id)data;

@end

Loose Coupling Between The View Controller and Data Source

Now we have a class that fetches data from the API. This is very powerful because we should be able to write our view controller code without worrying about listings at all. However, we still need to figure out the best way to connect the ListingNetworkSource to the FeedViewController . There are several approaches to connecting things in iOS apps.

We could consider using Key-Value Observing and have the FeedViewController observe the objects on the network source. With KVO a method (observeValueForKeyPath) would get called every time the objects change. The updates are triggered immediately, which may not give us the control that we need with timing. Ideally we can do parsing and filtering of the data from a background thread and not block the UI. Finally, this wouldn’t give us the semantics of knowing how the data changed (fetching new data, fetching more data), only that the data needed to be reloaded.

Another option would be to post a notification with NSNotificationCenter . This solves the thread safety issue for the most part, but it also limits the types of APIs we can build. Posting notifications is one-sided, so the posting class doesn’t receive any data back. This allows very minimal coupling, but the downside is that it can be difficult to follow the flow of the code since any object can observe the notifications. Furthermore, we wouldn’t be able to do filtering and parsing very easily with this technique since we can’t receive data back directly. Fortunately, there is another option: we can use delegation.

Delegation allows us to maintain loose coupling between the controller and the network source, but also give both sides some flexibility. The ListingNetworkSource doesn’t need to know what the controller is doing, but it can ask the controller questions like whether to filter an object, or send a message to say that new data has been fetched. Another advantage of delegation is it is easy to extend with new functionality. When the requirement to filter NSFW posts from the main feed surfaced, it was as simple as adding a new delegate method: listingNetworkSourceShouldFilterObject .

Here is a sample of the main parts of ListingNetworkSourceDelegate :

@protocol ListingNetworkSourceDelegate <NSObject>

- (void)listingNetworkSourceDidFetchData:(ListingNetworkSource *)listingNetworkSource;

- (void)listingNetworkSourceDidFetchMoreData:(ListingNetworkSource *)listingNetworkSource;

- (void)listingNetworkSourceDidFail:(ListingNetworkSource *)listingNetworkSource;

@optional

- (void)listingNetworkSourceDidParseNewData:(ListingNetworkSource *)listingNetworkSource;

- (void)listingNetworkSourceDidStartFetching:(ListingNetworkSource *)listingNetworkSource;

- (BOOL)listingNetworkSourceShouldFilterObject:(id)object;

@end

Putting It Together

ListingNetworkSource created an abstraction for fetching lists of data from the API; now we need to figure out how to connect it to the front end of the app. We could implement the methods for ListingNetworkSourceDelegate directly in FeedViewController . In object-oriented programming there’s a common saying to favor composition over inheritance. The FeedViewController implements the protocol and gets the functionality of the network source through composition. The problem is that if we want to reuse the listing code for comments and subreddits we have to implement the same delegate methods there too. This leads to a lot of code duplication since all of these cases need the exact same fetching, pull-to-refresh, and reloading functionality.

At the root of the class hierarchy we created BaseViewController as a subclass of UIViewController . In the base class, we implement core functionality such as screen view analytics and our customized navigation. For example, to allow the swipe gesture to pop back to the previous view controller from anywhere rather than only on the screen edge, we implemented a custom gesture recognizer in BaseViewController .

On top of BaseViewController there is also code that is common to all listings which we implement in ListingViewController . First, the ListingViewController implements all of the delegate methods of ListingNetworkSource . Second, the controller handles general purpose functionality for displaying all types of listings such as pull to refresh and fetching more content when you are nearing the bottom of the feed. With this in place, everything you need to build a listing is given to subclasses for free, and all we need to set up in the our view controller classes is the unique UI for each different type of content. For example, FeedViewController implements the feed posts, while MessageListingViewController implements the feed for private messages.

Building the UI

To architect the user interface for the feed there were similar decisions to make. There are many different types of posts on Reddit such as links, self-posts, images (of cats), gifs, videos (of cats), and more (cats). Each post will be a row in the collection view for the feed, in other words a UICollectionViewCell .

The most obvious approach is to make a UICollectionViewCell subclass for each type of post, but there are a couple of disadvantages to that approach. First, it would be nice to be able to reuse the post UI outside of a collection view. The solution is to create a simple wrapper cell, and put a regular UIView inside of it. This is a subtle distinction, but it makes the code more reusable for a little extra work.

It’s also the case that there is a lot of reused UI in each type of post. They all have a title bar, for instance, so we can create a view called FeedPostTitleView . We also want to put a bar with the vote score and comment count on every item in the feed, so we can create a view called FeedPostCommentsBarView .

Now we can create views for the individual types of posts FeedPostImageView , FeedPostVideoView , etc. Each of these serves a single purpose now, so they can just be plain UIViews as well. Building this way, the hierarchy of classes is very flat, yet we get maximum reusability as well.

Simple Optimizations To Feed Performance

After the first release we received numerous questions about how we achieved such good performance for the feed in the initial version of the app. One option is to integrate a library like Texture (formerly AsyncDisplayKit) or Facebook Component Kit to make the feed perform well. However, it is non-trivial to build the feed based on those open source framework due to our own Reddit specific challenges (apart even from licensing challenges). We always prefer the simple solution first. Instead of locking ourselves into dependencies, this lets us iterate fast and is easier for new hires.

With that said, after a few caching and layout optimizations the performance is good enough. The Reddit feed is laid out entirely with the old-school technique of calculating frames and caching cell heights manually. Each item in the feed implements a class method +(CGSize)calculateSizeWithData:(id)data , which takes the data for the item and returns the size at which it should be displayed.

We call this method from a background thread before the items are even displayed on screen and cache any expensive parts of the calculation. We do this because the collection view height methods are always called while scrolling, so by precalculating the height we assure no expensive calculations will be run on the main thread.

To be specific, for laying out text we have a class TextHeightCache which, given a font and a string and a layout width, returns the height of the text.

For images, there are some additional optimizations. Apart from using AFNetworking, which handles the image downloading, NSURL file caching and in-memory cache of decoded images, we also make sure to select the optimal image for the screen size and ensure that image decoding is done off the main thread.

By keeping the layout code simple, exercising caching, and keeping the main thread unblocked as much as possible, we found that we were able to achieve 60 frames per second.

Summary

When we set out to build this product we knew it was not an MVP; we were confident that millions of Reddit’s 250+ million monthly visitors would download the app and try it. Since the most essential functionality of the app is its feeds, we worked hard to build a flexible yet not complicated feed architecture and create building blocks that could be reused throughout the app. Finally, it’s often said that you only get one chance to make a first impression, so we worked hard to optimize performance in simple ways.

It is important to note that we intentionally keep the iOS team at a small size to enable high velocity and strong collaboration. As we hire more and support more product verticals in the app, new challenges will surface. At Reddit, we are currently looking for people who are interested in this level of challenge, so if you are one of them, please reach out to us.