There’s a new user experience that’s becoming more prevalent among apps these days. It’s a paging carousel of cards that looks much like the screen seen here:
Users are placed on a screen with a card in front of them and (when applicable) cards on either side of the first one. The cards on the side peek in, hinting to the user that one can swipe left or right to see more. Users can interact with the card in front of them by swiping up, which opens the current card for full page viewing and scrolling.
When a user wants to get back to the cards they first saw, all they have to do is swipe back down until the card collapses fully. Then they can continue swiping left and right to move through cards, or swipe up again to open a card they want to see more of.
So, as a developer, how do you achieve this kind of UX? There are plenty of ways, but let’s start simple. In the first part of this blog post, we’ll focus on the first major interaction - expanding of the “card” into “full screen mode,” where the user can scroll through its expanded content.
First, let’s take a look at the layout of the interface file (the nib). This part is the foundation of this user experience, and without the proper layout, this task becomes much more arduous than it needs to be.
This github repository I’ve created shows off this tutorial that you can add to your project or reference, should you like. This blog post assumes you have access to the code, can become familiar with it, and walks you through the high level technical challenges of such a user experience. What it does not do is step you line by line through the code or re-write the code. That’s your homework ;-)
The view hierarchy should look something like this:
There’s a container view (called “big container view” in my code) that is constrained to the bounds of the view controller’s view. This is where the visible guts of the view controller will live. Above the “big container view,” we have a UIScrollView, which I’ve called “big scroll view.” This sits on top of everything else in the view controller and detects scroll events, so that we can then control the expanding, collapsing, and scrolling of the “card.” This is also constrained to the bounds of the view controller’s view.
Within the “big container view” is another container view, which I’ve called “small container view.” This houses the “card” portion of the view controller, which I have implemented as a table view (mine is called “tableView” in the demo). It’s important to note that this “card” could be any kind of scroll view (and not just a UITableView).
The main thing here is that your “card” has scrollable content that the user can pan through. I’ve done this in the form of supplying 100 relatively plain table view cells to the table view. This ensures that the user can scroll down the table for a while. You can make your card’s content equal to whatever you like.
The blue lines seen in the screenshot correspond to the top, left, bottom, and right constraints of the “small container view.” Their sizes can be of any value(s) you please, depending on your design. Just make sure they constrain the small container view to its superview - the “big container view.” The manipulation of these constraints is how we will achieve the growing and shrinking effect of the card. More on that later.
(IMPORTANT: If you’re implementing this on your own, don’t forget to make your view controller the delegate of the scroll view and table view. I know I make this mistake a lot and it can be hard to debug since it’s often done in interface builder, where it’s hidden from plain sight).
Now that you know how the layout looks, we’ll move onto the “technical” challenge of the UI: making the card actually grow, shrink, or pan with each scroll. While this is the most technical / tricky portion of the demo, it’s not actually difficult to implement. Conceptually, we want to detect all scrolls on the view, but we don’t always want to scroll the content of the table view.
So, how we do this? It’s relatively simple, and it (almost) all happens in one place - the UIScrollViewDelegate’s scrollViewDidScroll: method. The logic is as follows: if the user has not yet scrolled far enough to make the “card” expand all the way out, then update the card’s top, left, bottom, and right constraints so that the card appears to expand / contract appropriately. In addition, set the tableView’s content offset to zero since we don’t want the content to scroll yet.
If the user has scrolled far enough to expand the card all the way out, set the tableView’s content offset to be whatever the scrollView says it is, minus the distance the user scrolled in order to expand the card all the way out. This gives the effect of “normal” scrolling once the card has been expanded all the way.
The last thing you’ll need to do is manipulate the contentSize of the “big container view.” You’ll want to keep its width the same, but for its height, you’ll want it to be equal to the content size of the table view plus the scroll distance required to enter “full screen mode.” This tells the “big scroll view” that it should keep scrolling all the way down to the bottom of the table view. Without making this change, your “big scroll view” wouldn’t scroll at all!
There are a couple other final touches I’ve added to the code that I won’t expand upon here, such as: rounding the corners of the card and preventing the card from getting stuck in a state between fully expanded or fully collapsed after scrolling. They are relatively straightforward, but aren’t that relevant to the core swiping challenge we solved in this post, so for those reasons, I’ve left them out of this write up (the code can be found here).
Though it’s less than 200 lines of code, a lot of thought went into the implementation of it. In a future post, we’ll talk about bringing it all together and being able to swipe between cards. Until then!