Vertical Paging Without Limitations

Let's connect

Thank you! We'll get in touch soon

Most mobile app users take scrolling for granted. As they absentmindedly use their fingertips to scroll endlessly through newsfeeds on Facebook, Instagram, Twitter, LinkedIn, Google Newsstand, or countless other apps, they don’t stop to think about the carefully constructed code that supports that seemingly effortless scrolling.

In fact, there’s quite a bit of work that goes into creating a seamless scrolling action that delivers a customized, streamlined user experience. Here at Distillery, we enjoy a good challenge, so we were ready and willing when we realized there was no off-the-shelf solution that would support the scrolling experience we wanted for the iOS Soapbox app we were building. This blog repost recounts how we tackled the challenge. Written by Nikolay Sohryakov, one of Distillery’s developers, the article was first published on Distillery’s blog in December 2016.

The Soapbox app for iOS provides a fun, simple way to capture moments, share them with the world, and help promote items and causes. Soapbox emphasizes the ability to monetize your social presence, too. You can follow your friends and discover the brilliant creators from all over the world who are sharing the amazing content you love.

While developing the app, though, Distillery encountered a number of technical challenges that we needed to overcome in order for the app to work the way it was envisioned to work. In this post, we’ll talk about one challenge in particular.

The Challenge

Soapbox is a content-centric app that’s built around a newsfeed. However, there are certain features of a traditional newsfeed implementation that were not quite in line with the experience we wanted users to have:

A newsfeed as implemented in an app like Facebook offers a continuous scrolling experience, but we wanted users to be able to swipe up or down to snap directly to the next (or previous) post in the feed.

We wanted only one newsfeed post to appear on the screen at a time — even if the post did not fill the whole screen.

We also wanted posts to be of any length, so a post might actually extend beyond a single screen (and you would be able to scroll down to see more content by dragging).

We wanted users to be able to swipe between posts in order to move posts around like cards.

The question, then, was how to achieve these user experience goals.

The Solution

The first idea for a solution to these challenges was to use a UITableView with isPagingEnabled set to true. But that proved not to be a viable option, because UITableView requires pages to be the same size. We surfed the web in search of a ready-to-use solution and found nothing – so, we decided to build our own control, one that would be fully customizable and that would meet all of our needs. That control we’re calling CardScrollView.

Creating CardScrollView

Because we wanted Soapbox to have a scrolling content feed, we decided to use UIScrollView as the basis for our new control. UIScrollView actually implements the underlying scrolling logic and exposes all the APIs needed to control the scrolling process. But what about the content inside the UIScrollView? One thing that had been attractive about UITableView was that it manages memory by itself and does a great job reusing cells. So how could we do that for CardScrollView?

We thought back to a conversation held at the 2010 Apple Worldwide Developers Conference. There, Apple engineers talked about Designing Apps with Scroll Views. A very important part of this talk explained how to reuse views in UIScrollView through the implementation of an Object Pool Pattern. That was it! That was the approach we needed to take for CardScrollView.

Setting Up the Framework

To make CardScrollView reusable, we needed to set it up as a framework. That required the creation of a new Xcode project built upon the Cocoa Touch Framework.

By setting this up first, we would have a ready-to-use framework binary. Then, we began to write some code!

Implementing the Framework

For the CardScrollView control, we needed two classes:

A class representing the cell (we call it a card)

A scroll view itself (we call it CardScrollView)

Implementing the cell class is pretty straightforward, so here we’ll focus on how we implemented the second of these classes, the CardScrollView.

Building the CardScrollView Class

First of all, we needed to implement the cards’ layout logic and get it all working together. We needed to keep a pool of reusable views, with each view representing a single card. To make it possible to use different views for different kind of data, we introduced a reuse identifier which works the same as reuse identifier in UITableView.1Note: To make control user friendly and easy to use, we tried to keep the naming conventions very close to those that Apple has for UITableView. So, a lot of methods are called in a similar way.

In this implementation, reuseIdentifiers is a dictionary that matches a reuse identifier with a representing object and reusePool is defined as an array.For better understanding of how this works, let’s take a look at the implementation of two methods below:

This code builds an array of CardDetails instances that capture the parameters we will need to perform a proper layout. A CardDetails is a struct that records the starting position of a card, its height, and a pointer to the presenting card if it’s visible. As the code shows, we limit the minimum height of the card to the scroll view height, so our cards will always have a height equal to or greater than scroll view height.

Laying Out the Cards

Having collected all the required data in one array, the process of laying out the cards becomes pretty straightforward. For each card that is visible (or is about to become visible) we calculate it’s Y coordinate and add it as a subview to a scroll view. When we are done laying out cards, the next step is to recycle those cards that are no longer visible:

There is one important thing to keep in mind about this method: It’s going to be called a lot — every time content is scrolled, every time the device is rotated, or any time a similar action occurs. To achieve the responsiveness we wanted, we implemented a didSet observer for the contentOffset property of scroll view:

That’s all the work we needed to do to ensure proper card layout and reuse!

Handling the Scrolling

But what about the scrolling logic?

CardScrollView is inherited from UIScrollView. So, to gain control over the scrolling process, we need to set self as a UIScrollViewDelegate and implement the following delegate methods:

scrollViewWillBeginDragging – to track the beginning of the dragging process

ScrollViewWillEndDragging – to set up the correct target content offset

scrollViewDidEndDragging – to adjust the content offset in case a user tries a drag-and-release motion instead of swiping

scrollViewDidEndDecelerating – to handle corner cases

The scrollViewWillBeginDragging and scrollViewDidEndDecelerating methodsare responsible for tracking the content offset and retaining it as an internal variable. The work of setting up the scrolling offset itself is primarily done by the scrollViewWillEndDragging method.

To put this all in play, the first thing we need to do is determine a scroll direction. That’s easy to discover if we have two distinct offsets — a target content offset (which we call targetContentOffset) and a current offset (which we call lastContentOffset). By subtracting the one from the other we get a delta value, which we call scrollDelta:

This actually provides all the data we need to calculate which cell index will be visible next. If the scrollDelta value is greater than zero, the next cell will be focused; if less than zero, the previous cell will be focused.

Finally, there is one more consideration: If a card height is more than minimum allowed (which is CardScrollView height), then we need to attach to its top or bottom edge, depending on the scroll direction. This is easily done by changing offset by height delta:

By testing to see whether a given card is longer than the card height, this bit of code ensures that a down swipe gesture actually scrolls down into the next portion of card content rather than jumping to the next card. Once a user reaches the bottom of the card, a down swipe jumps to the next card. Conversely, an up swipe from the bottom of a long card would scroll backwards in the card and before jumping to the previous card.

Handling the Edge Cases

This code will cover 90% of scrolling cases. The remaining 10% are edge cases that require special attention:

User dragged instead of swiping up/down. In this case, there will be no deceleration animation and the code above will not perform its job because the target content offset is the same as last content offset, and the scroll delta will equal 0.

When scrolling content with large cards. In this instance, it’s hard to calculate which cards will be on the screen and how to adjust them.

To handle the first edge case, we need to perform layout calculations when the user finishes dragging (which requires only the use of the scrollViewDidEndDragging method). To handle the second edge case we need to do the same in scrollViewDidEndDecelerating. This approach makes scrolling animations smoother and provides a “live” experience.

Summary

There’s more to the CardScrollView framework than we’ve shown here, but this is the essence of it. What we have not shown is mostly housekeeping stuff. You can find the full source, along with completely documented code and usage examples, on GitHub. The framework supports CocoaPodsand Carthage.

Development of the CardScrollView framework is ongoing, so if you have any questions or suggestions, feel free to contact us — or, create a Pull Request on GitHub if you have something cool to introduce!

Got an idea for a great new app? Contact Distillery about turning that idea into a marketable reality!