Unread's pull-for-menu

Deconstructing the evolution of revolution.

Background

In mid-2013 the RSS world was dealt a substantial blow. Google announced that their (the) RSS subscription service, Google Reader, was to be shuttered. With it, millions of voices suddenly cried out in terror, and were suddenly silenced.

Declining use was cited as the key reason for the shutdown, though the immense reaction from Google Reader users indicated that this was a service which still maintained a large audience. The web was abuzz with concern for the future of RSS and the open web in general, though there was also a sense of optimism, a chance for those without the resources of a giant like Google to gain a foothold in the vacuum of what was once a tightly dominated market. To build a worthy replacement for the Google Reader exiles.

Despite what could’ve been its death knell, RSS is alive and well today with services like Feedly, Feedwrangler and Feedbin filling the void left by Google Reader’s demise. Along with it came a new breed of modern iOS RSS readers. Among them is Unread, a delightfully clean and easy to use client for the aforementioned services, built by Jared Sinclair. In the short time it has spent on the app store, it has garnered a considerable following, so considerable that there’s a good chance you’re reading these very words from Unread.

This article is about Unread’s pull-for-menu interaction, but it is also about history, how far we’ve come and how we got here.

Landscape

If we were to plot the landscape of news and content aggregation apps on iOS, we might plot apps like Flipboard and Pulse (now LinkedIn Pulse) at one end of the scale, where the experience drives not only content consumption but content discovery. These are the apps you imagine yourself using when you sit down on a Sunday morning with a coffee (tea for those in the antipodes) and get lost in the magazine experience.

On the opposite end of that scale we have apps like Reeder, apps that are about consuming content in the most efficient way, the apps you use to escape the monotony of a daily commute or rid yourself of FOMO. This is where one might plot Unread.

Unread continues a theme of restraint we’ve discussed previously. The way it bills itself is simple: you sign in to your RSS aggregation account of choice and you read. That’s it. Within this spartan ethos Unread provides an experience designed and engineered for single handed use.

To really appreciate where Unread’s menu interaction is coming from, let’s get Darwinistic.

Evolution

If we look back to Tweetie, an app regarded as an icon of iOS development, it introduced us to the the now commonplace pull-to-refresh pattern. Pull-to-refresh became so accepted, even expected, it was validated by Apple and adopted as the default mechanism for refreshing your Mail.app inbox.

Then there was the Facebook iOS app which popularized the navigation drawer (aka “God Burger”, “Burger Basement” and many other epithets). While they’ve since removed this for the navigation (it still remains for contacts), the extent at which it spread throughout the iOS design landscape made it a conventional, accepted pattern.

Fast forward to today and we have Unread’s menu, an amalgam of these two accepted, conventional patterns. It’s the evolution of two revolutionary interactions which set a precedent for how we interact with our devices.

Unread provides a tutorial on first launch which explains how to present the menu, though one could argue that it isn’t needed. It is a product of its lineage and as such, can rely on a certain level of expectation and understanding that that lineage has established.

Deconstructing

This year’s WWDC yielded a bumper crop of new shiny for developers to play with: UIKit Dynamics, Text Kit, Sprite Kit and UIViewController transitions to name but a few. We’ll be using two of these to recreate Unread’s menu, UIViewController transitions and UIKit Dynamics, although the latter we won’t be dealing with directly.

The first thing we notice when we pull the content to expose the menu is the spring in the pull indicator. Contrasted against the focussed, understated reading interface of Unread it’s hard to miss. It’s reminiscent of the (sadly) short lived iOS 6 pull-to-refresh animation1, delightfully playful and descriptive of the interaction progress.

We’ve covered similar dynamic behaviour in the past and implemented it using UIKit Dynamics, this time around we’ll step up an abstraction layer.

That 7 parameter method

One of the great features of Objective-C is named parameters. Coupled with the verbosity of the language, it provides a natural way to describe and document the intent of a method, though the length of some methods can scare some new developers. One such method is the newly added UIView block based animation method, animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion: which, while not the longest method in Cocoa Touch, is certainly on the scoreboard.

Despite its imposing presence, it’s an incredibly simple to use yet powerful method for adding dynamic animated behaviour to your interface without the overhead of pulling in a full UIKit Dynamics stack. It was noted by some observant readers that the dynamic behaviour in a previous post may have been implemented using this method, so it seems like a great opportunity to put it to work on the pull-for-menu spring behaviour.

Stretttch

If you imagine stretching a out a rubber band, the further it is stretched, the thinner the band will become. This physical behaviour is mirrored in Unread’s pull interaction and while it’s a small detail, one that you mightn’t notice unless you were looking for it, it strengthens the perception that when we’re dragging the scroll view beyond its contentSize, we’re met by resistance.

To mimic this behaviour in our implementation, we’ll be providing a view (SCSpringExpandingView) that will animate between two different frames. The view’s frame for our collapsed, un-expanded state will take full width of its superview with a height that matches, giving us a small square view.

- (CGRect)frameForCollapsedState
{
    return CGRectMake(0.f, CGRectGetMidY(self.bounds) - (CGRectGetWidth(self.bounds) / 2.f),
                      CGRectGetWidth(self.bounds), CGRectGetWidth(self.bounds));
}

When we stretch our view into its expanded state, we’ll use a frame that is the height of the superview but only half the width. We’ll also shift the horizontal origin across so our view stays within the centre of the superview.

- (CGRect)frameForExpandedState
{
    return CGRectMake(CGRectGetWidth(self.bounds) / 4.f, 0.f,
                      CGRectGetWidth(self.bounds) / 2.f, CGRectGetHeight(self.bounds));
}

To round the corners of our view, we’ll set the cornerRadius of our stretching view’s layer to be half the width of the view, giving it a circular appearance when collapsed and a rounded edge when expanded. We’ll need to update this value when we transition between our collapsed and expanded states as we modify the width of the frame, otherwise one of the cases would have a rounded edge that would be contrary to the width of the view.

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.stretchingView.layer.cornerRadius = CGRectGetMidX(self.stretchingView.bounds);
}

Now all that is left to do is to animate between the two states using our new friend with the long name, animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:.

We’ve seen most of the parameters used in this method before, but let’s take a quick look at the two that matter to us, usingSpringWithDamping and initialSpringVelocity.

usingSpringWithDamping takes a CGFloat value between 0.0 and 1.0 and determines the oscillation of the spring, in a physical sense, the strength of the spring. A value closer to 1.0 will increase the strength of the spring and result in low oscillation. A value closer to 0.0 will weaken the strength of the spring and result in high oscillation.

initialSpringVelocity also takes a CGFloat however the value passed will be relative to the distance travelled during the animation. A value of 1.0 translates to the total animation distance traversed in 1 second while a value of 0.5 translates to half the animation distance traversed in 1 second.

While these parameters correspond to physical properties, for the most part it’s a case of if it feels good, do it.

[UIView animateWithDuration:0.5f
                      delay:0.0f
     usingSpringWithDamping:0.4f
      initialSpringVelocity:0.5f
                    options:UIViewAnimationOptionBeginFromCurrentState
                 animations:^{
                     self.stretchingView.frame = [self frameForExpandedState];
                 } completion:NULL];

And that’s it. With just one method call and some waving of magic numbers, we can take advantage of the dynamic underpinnings of UIKit in iOS 7.

Three’s a crowd

Now that we’ve created our SCSpringExpandingView, we’ll need to create a view that houses the three SCSpringExpandingViews. Let’s call it SCDragAffordanceView.

The basic job of the the SCDragAffordanceView will be to layout the three SCSpringExpandingView’s as well as providing an interface with which we can pass in the progress of pull-for-menu interaction.

To layout our SCSpringExpandingViews, we’ll override layoutSubviews and align each of the views frames equally spaced across our bounds.

- (void)layoutSubviews
{
    [super layoutSubviews];

    CGFloat interItemSpace = CGRectGetWidth(self.bounds) / self.springExpandViews.count;

    NSInteger index = 0;
    for (SCSpringExpandView *springExpandView in self.springExpandViews)
    {
        springExpandView.frame = CGRectMake(interItemSpace * index, 0.f, 4.f,
                                 CGRectGetHeight(self.bounds));
        index++;
    }
}

Now that we have the views laid out, we’ll need to update them when someone calls the setProgress: method. If we look back to Unread, we can see three distinct states for each of the spring views: Collapsed, expanded and completed. The first two we’ve mentioned, but the final is what indicates that the pull-for-menu interaction has reached a point where releasing will trigger the menu to be shown.

To implement this, we’ll iterate over our three SCSpringExpandingViews and update the colour of each based primarily on whether the progress passed in is greater or equal to 1.0, followed by whether the progress is great enough that the view should expand.

- (void)setProgress:(CGFloat)progress
{
    _progress = progress;

    CGFloat progressInterval = 1.0f / self.springExpandViews.count;

    NSInteger index = 0;
    for (SCSpringExpandView *springExpandView in self.springExpandViews)
    {
        BOOL expanded = ((index * progressInterval) + progressInterval < progress);

        if (progress >= 1.f)
        {
            [springExpandView setColor:[UIColor redColor]];
        }
        else if (expanded)
        {
            [springExpandView setColor:[UIColor blackColor]];
        }
        else
        {
            [springExpandView setColor:[UIColor grayColor]];
        }

        [springExpandView setExpanded:expanded animated:YES];
        index++;
    }
}

Now that we’ve covered some of the new hotness, let’s take a detour down a well travelled road.

Nested UIScrollView

Ask any iOS developer and they’ll tell you, nested scroll views are the user interface element, so much so that Apple have dedicated a chapter of their UIScrollView programming guide to the topic. It’s criminal that we’ve studied this many innovative iOS interfaces together without mentioning them.

For our example content, we’ll be displaying some riveting Lorem Ipsum with UITextView, a class which received some TextKit love in the great facelift of iOS 7. While we won’t be covering any of the new APIs in this entry, anyone interested should check out the fantastic writeup on objc.io. Instead, all we need to remember is that UITextView is a subclass of the mighty UIScrollView.

We want our SCDragAffordanceView to always be at hand, ready to present our menu. One option to consider would be to add it as a subview of our UITextView and modify its vertical origin based on the contentOffset of our UITextView, but this overloads the responsibility of our UITextView beyond just displaying text and just feels a bit wrong.

Instead lets create a separate instance of UIScrollView which our UITextView and SCDragAffordanceView will be added as subviews of.

self.enclosingScrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.enclosingScrollView.alwaysBounceHorizontal = YES;
self.enclosingScrollView.delegate = self;
[self.view addSubview:self.enclosingScrollView];

The key line here is setting alwaysBounceHorizontal to YES. Now regardless of the contentSize of the scroll view, dragging horizontally will always continue beyond the bounds with the expected resistance.

If our nested UITextView’s horizontal content size doesn’t exceed its bounds, then we’ll achieve the effect of having just one UIScrollView, while separating concerns in our code.

We’ll also want to become the delegate of the scroll view so that we detect the scroll view is being dragged and update our SCDragAffordanceView’s progress accordingly.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView.isDragging)
    {
        self.menuDragAffordanceView.progress = scrollView.contentOffset.x /
                                             CGRectGetWidth(self.menuDragAffordanceView.bounds);
    }
}

Finally when we receive the scrollViewDidEndDragging:willDecelerate: delegate callback, we’ll use that same progress we calculated in the scrollViewDidScroll: callback to determine whether to present our menu view controller. If not, we’ll set our progress back to 0.0.

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (self.menuDragAffordanceView.progress >= 1.f)
    {
        [self presentViewController:self.menuViewController
                           animated:YES
                         completion:NULL];
    }
    else
    {
        self.menuDragAffordanceView.progress = 0.f;
    }
}

With that dusty road behind us, let’s get stuck into the next piece of iOS 7 hotness.

UIViewControllerTransitioningDelegate

What a difference a version makes. Had this post been written before iOS 7, it would have been a much longer and caveat filled affair. Previously if you wanted behaviour like Unread’s pull-for-menu, you’d have to insert your view atop the current view controller, window, or some other equally smelly behaviour. While this would give you the desired effect, it always felt as though you were going against the grain of the framework.

Thankfully in iOS 7, Apple noticed this pattern emerging and took another cue from the developer community, providing a clean, sanctioned way to achieve this using a set of minimal protocols. You can now define custom animations and interactive transitions between view controllers by implementing the UIViewControllerTransitioningDelegate protocol.

This UIViewControllerTransitioningDelegate protocol declares a handful of methods which allow you to return animator objects that define one of the three phases of view transition: presenting, dismissing, and interacting. Our custom transition will be defining the presenting and dismissing phases.

In our view controller, we’ll declare that we conform to the UIViewControllerTransitioningDelegate protocol and implement the two methods we care about, animationControllerForPresentedController:presentingController:sourceController: and animationControllerForDismissedController:.

Now that we provide the callbacks for a custom view controller transition, we need a view controller to present. Unread’s neat menu item animations are out of the scope of this article, so for our case we just need to create a view controller (SCMenuViewController) to be presented when the menu interaction is triggered.

self.menuViewController = [[SCMenuViewController alloc] initWithNibName:nil bundle:nil];

Once we’ve created an instance of this class, we need to set its transitionDelegate to be our view controller and set its modalPresentationStyle to UIModalPresentationCustom so that it calls back to its transitioningDelegate when presented.

self.menuViewController.modalPresentationStyle = UIModalPresentationCustom;
self.menuViewController.transitioningDelegate = self;

Now when we present our menu view controller, it will callback to its transitioningDelegate (our view controller) to ask for the presenting UIViewControllerAnimatedTransitioning animator object.

UIViewControllerAnimatedTransitioning

To provide our animator objects to our menu view controller, we’ll start by creating a plain old NSObject subclass called SCOverlayPresentTransition, and declare that it conforms to the UIViewControllerAnimatedTransitioning protocol. In our animationControllerForPresentedController:presentingController:sourceController: delegate callback, we’ll create an instance of our SCOverlayPresentTransition object and return it.

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [[SCOverlayPresentTransition alloc] init];
}

For the dismissal animation, we’ll create another NSObject subclass called SCOverlayDismissTransition and provide an instance of it when we receive the animationControllerForDismissedController: delegate callback.

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[SCOverlayDismissTransition alloc] init];
}

The implementation of our present and dismiss transition objects consist of two methods, transitionDuration: and animateTransition:. The transitionDuration: method as you may have guessed simply requests an NSTimeInterval to dictate the duration of the animation. The animateTransition: is where the real work of the transition is done.

The sole parameter of the animateTransition: method is an object conforming to the UIViewControllerContextTransitioning protocol. From this object we can pluck out the objects and information we need to drive our animation, including the view controllers involved in the transition. It also provides methods that we’ll use to notify the framework that we’ve completed our transition.

UIViewController *presentingViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *overlayViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

Once we have the presenting and presented view controllers, we need to add their views as subviews of the transition’s container view so that they both appear during the animation.

UIView *containerView = [transitionContext containerView];
[containerView addSubview:presentingViewController.view];
[containerView addSubview:overlayViewController.view];

The final piece of the presenting transition is to simply animate the views however we fancy, then notify the transitionContext object whether we’ve completed our transition successfully.

overlayViewController.view.alpha = 0.f;
NSTimeInterval transitionDuration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:transitionDuration
                  animations:^{
                     overlayViewController.view.alpha = 0.9f;
                 } completion:^(BOOL finished) {
                     BOOL transitionWasCancelled = [transitionContext transitionWasCancelled];
                     [transitionContext completeTransition:transitionWasCancelled == NO];
                 }];

The SCOverlayDismissTransition will follow largely the same process, albeit in the opposite direction.

Now when our menu view controller is presented it will use our custom transition, keeping the presenting view controller’s view in the view hierarchy.

Closing

As we approach the 6th anniversary of the iOS App Store its amazing how far the app landscape has come. The idea that we can already consider apps as classics is an indication of just how fast it’s moving. Every year developers are given a new collection of toys to build great apps with, yet there is still room for the venerable UIScrollView.

You can checkout this project on GitHub.

  1. If you’re feeling nostalgic for the heady iOS 6 days, there’s a great clone of the iOS 6 pull-to-refresh control on GitHub