# Deconstruct the evolution of revolution

background

In mid-2013, the RSS world took a beating. Google announced that their RSS feed service, Google Reader, was shut down. With it, millions of voices suddenly cry out in horror and suddenly fall silent.

A drop in usage was the main reason for the closure, although the huge response from Google Reader users suggests the service is still attracting large numbers of users. The web is full of worries about the future of RSS and the open Web as a whole, although there is also a sense of optimism that those without the resources of a giant like Google have a chance to establish themselves in a tightly controlled market in what was once a vacuum. Build a valuable alternative to Google Reader exiles.

Despite what may be the death knell, RSS is alive and well today, with services such as Feedly, Feedwrangler, and Feedbin filling the void left by the demise of Google Reader. Along came the new modern iOS RSS readers. One of them is Unread, a delightfully clean and easy to use client built by Jared Sinclair that provides the above services. In the short time it has spent in the app Store, it has already attracted enough followers that there is a good chance that you will read these words from Unread.

This article covers Unread’s menu interaction, but it also covers history, how far we’ve come, and how we got here.

landscape

If you were to plot the landscape of news and content aggregation apps on iOS, you could plot apps like Flipboard and Pulse (now LinkedIn Pulse) at one end of the scale, where experiences drive not just content consumption but content discovery. These are the apps you imagine you’ll use when you get lost in the magazine experience on a Sunday morning as you sit down to drink coffee (and deal with those tea people).

Instead, we have apps like Reeder, which will consume content in the most efficient way, and apps you use to escape the monotony of your daily commute or get rid of FOMO. This is where it is possible to draw unread.

Before continuing our discussion of restraint. The billing itself is simple: You log into the RSS aggregation account of your choice and read it. And nothing more. In this Spartan spirit, unread offers an experience designed and manufactured for one-handed use.

To really understand where Unread menu interaction comes from, let’s meet Darwinistic.

The evolution of

If we look back at the app Tweetie, which is considered the icon of iOS development, it introduced us to the now-commonplace “refresh on demand” model. Pull-to-refresh has become so accepted that it can even be expected that it has been validated by Apple and used as the default mechanism for refreshing the mail.app inbox.

Then there was the Facebook iOS app, which popularized the navigation drawer (aka the “God Burger,” the “Hamburger basement” and many other upper-class people). Since they removed it from navigation (and kept it for contacts), its spread throughout the iOS design environment has made it a regular and acceptable pattern.

Fast forward to today, and we have the menu of Unread, a hybrid of two recognized traditional modes. These are two developments that revolutionize interactions and set precedents for how we interact with our devices.

Unread provides a tutorial on the first boot, which shows how to display the menu, although one might think you don’t need one. It is the product of its lineage and, therefore, can rely on a certain level of expectations and understanding that lineage has established.

deconstruction

This year’s WWDC brings many new highlights for developers: UIKit Dynamics, Text Kit, Sprite Kit and UIViewController transitions, to name a few. We’ll use two of them to recreate the Unread menu, UIViewController transitions and UIKit Dynamics, although we won’t deal with the latter directly.

The first thing we notice when pulling content to display the menu is pulling the spring in the indicator. With a strong contrast of emphasis, the low-key reading interface is hard to miss. This is reminiscent of the (brief) iOS 6 brief refresh Animation 1, which delightfully describes the interaction.

We’ve introduced similar dynamic behavior in the past and implemented it using UIKit Dynamics, but this time we’ll reinforce an abstraction layer.

The 7 parameter method

One of the things objective-C does is name parameters. Coupled with the verbosity of the language, it provides a natural way to describe and document the intent of a method, although the length of some methods can be intimidating to some new developers. One such method is the newly added animation method based on UIView blocks, AnimateWithDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion: the method is not Cocoa Longest method in Touch, but definitely on the scoreboard.

Despite its power, it’s a very easy to use but powerful way to add dynamic animation behavior to an interface without having to extract the full UIKit Dynamics stack. Some careful readers have noticed that the dynamic behavior in previous posts might have been implemented using this method, so it seems like a good opportunity to use it for the spring behavior of line wrapping by menu.

Stretttch

If you want to stretch a rubber band, the deeper you stretch the band, the thinner it becomes. This physical behavior is reflected in the pull interaction of Unread, and while it’s a small detail that you might not notice unless you’re looking for it, it enhances the feeling that we’re being resisted by contentSize when we drag the scroll view over it.

To mimic this behavior in our implementation, we will provide a View (SCSpringExpandingView) to animate between two different frames. The folded, unexpanded view frame takes up the entire width of its parent view and is highly matched, 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));
}
Copy the code

When we stretch the view to the expanded state, we will use a frame that is the height of the superview, but only half the width. We will also move the horizontal origin to keep our view in the center of the superview.

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

To make the corners of the view round, we set the layer of the cornerRadius stretch view’s layer to half the width of the view so that it has a rounded appearance when folded and a rounded edge when expanded. When changing the width of the frame, we need to update this value as we transition between collapsed and expanded states, otherwise the edges in one of the cases will become rounded corners, which is the opposite of the width of the view.

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

The only thing left to do now is to use our new friends long name to set up animation animateWithDuration between two states: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: Completion:.

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

UsingSpringWithDamping ‘ ‘CGFloat takes a value from 0.0 to 1.0 and physically determines the strength of the spring. A value near 1.0 will increase the strength of the spring and cause low vibration. Values near 0.0 weaken the spring and cause high vibration.

InitialSpringVelocity will also accept CGFloat but the value passed will be relative to the distance traveled during the animation. A value of 1.0 represents the animation distance traversed in 1 second, while a value of 0.5 represents half the animation distance traversed in 1 second.

Although these parameters correspond to physical properties, they feel good in most cases, so do this.

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

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

Three people are

Now that we have created the SCSpringExpandingView, we need to create a view that contains three SCSpringExpandingViews. Call it SCDragAffordanceView.

The Basic work of the SCDragAffordanceView is to lay out three ScSpring ExpandingViews and provide an interface through which we can interact with drop-down menus.

To layout the SCSpringExpandingView, we will overwrite the layoutSubviews and align each view frame so that it is evenly spaced on the boundaries.

- (void)layoutSubviews
{
    [super layoutSubviews];

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

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

Now that we have laid out our views, we will need to update them when someone calls the setProgress: method. If we look back at unread, we can see three different states for each spring view: collapsed, expanded, and completed. We’ve already mentioned the first two, but the last one is the point indicating that the menu-pull interaction has reached the release point that will trigger the display of the menu.

To do this, we’ll iterate through the three SCSpringExpandingViews and update the color of each S primarily based on whether progress is passed in greater than or equal to 1.0, and then based on whether S progress is large enough for the view to be extensible.

- (void)setProgress:(CGFloat)progress { _progress = progress; CGFloat progressInterval = 1.0 f/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++; }}Copy the code

Now that we’ve covered some of the new hot spots, let’s bypass a less traveled road.

Nested UIScrollView

Ask any iOS developer and they’ll tell you that the user interface elements of nested scroll views are so much so that Apple has devoted a chapter to the topic of their UIScrollView program guide. We’ve looked at so many innovative iOS interfaces together that it’s a crime not to mention them.

For our sample content, we’ll show off some of UITextView’s appealing Lorem Ipsum, which gained some TextKit love in the new iOS 7 redesign. While we won’t cover any new apis in this entry, those who are interested should check out the excellent articles on objc.io. Instead, we just need to remember that UITextView is a powerful subclass of UIScrollView.

We hope our SCDragAffordanceView is always with you, ready to showcase our menu. One option to consider is to add it to our subview based on UITextView and modify its vertical origin contentOffset to our UITextView, but this overload is our responsibility for UITextView not just to display text, it just feels a bit off.

Instead, let’s create a separate instance of UIScrollView, and our UITextView and SCDragAffordanceView will be added as child views.

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

The key line here sets alwaysBounceHorizontal to YES. Now, regardless of the contentSize scrolling view, dragging horizontally will always continue out of bounds with the expected resistance.

If we nested the horizontal content size of the UITextView within its range, we would have the effect of only one UIScrollView, while separating concerns in the code.

We also want to be the delegate for the scroll view so that we can detect the scroll view being dragged and update the SCDragAffordanceView accordingly.

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

Finally, when we receive scrollViewDidEndDragging: willDecelerate: delegate callback, we will use the scrollViewDidScroll: the same as those calculated in the callback schedule to determine whether to display the menu view controller. If not, we set the 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; }}Copy the code

With dusty roads, let’s get into the next iOS 7 hot issue.

UIViewControllerTransitioningDelegate

What are the different versions? If this article had been written before iOS 7, it would have been a long and noteworthy affair. Previously, if you wanted to use behavior such as “unread” drop-down menus, you had to insert the view on top of the current view controller, window, or other smelly behavior. While this will give you the desired results, it always feels like you’re violating the requirements of the framework.

Thankfully, with iOS 7, Apple noticed the emergence of this pattern and took another cue from the developer community, which provides a clean, approved way to achieve this goal using a minimal set of protocols. Now, you can by implementing UIViewControllerTransitioningDelegate protocol to define custom animations and interactive transition between the view controller.

The UIViewControllerTransitioningDelegate agreement stating some method, these methods so you can return the animator objects, these objects are defined view one of the three stages of transition: present, closing and interaction. Our custom transition will define the presentation and release phases.

In our view controller, We will declare we observe and implement UIViewControllerTransitioningDelegate agreement we care about the two methods of animationControllerForPresentedController: presentingControll Er: sourceController: and animationControllerForDismissedController:.

Now that we have a callback for the custom view controller transition, we need a view controller to render. Unread, clean menu item animations are beyond the scope of this article, so for us, all we need to do is create a view controller (SCMenuViewController) that displays when a menu interaction is triggered.

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

After creating an instance of this class, we need to set its transitionDelegate to our view controller and modalPresentationStyle, UIModalPresentationCustom to transitioningDelegate can callback when appear it.

self.menuViewController.modalPresentationStyle = UIModalPresentationCustom;
self.menuViewController.transitioningDelegate = self;
Copy the code

Now, when we show menu view controller, it will be back to its transitioningDelegate (our view controller) to request show UIViewControllerAnimatedTransitioning animation object.

UIViewControllerAnimatedTransitioning

In order to provide animation objects to the menu view controller, we will create a plain old started SCOverlayPresentTransition NSObject subclass, and declared the UIViewControllerAnimatedTransitioning agreements are met. In animationControllerForPresentedController: presentingController: sourceController: delegate callback, We will create SCOverlayPresentTransition object instance and returns it.

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

For firing animation, we will create another is called a subclass of NSObject SCOverlayDismissTransition, and after receiving animationControllerForDismissedController: commissioned to provide its instance when the callback.

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

Our implementation of the present and recall transition objects includes two methods, transitionDuration: and animateTransition:. TransitionDuration: As you might have guessed, simply request NSTimeInterval to specify the duration of the animation. The animateTransition: is a substantial work in transition.

The animateTransition: the only parameter method is accord with UIViewControllerContextTransitioning the object of the agreement. From this object, we can extract the objects and information needed to drive the animation, including the view controller involved in the transition. It also provides methods for notifying the framework that we have completed the transition.

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

Once you have rendered and rendered view controllers, you need to add their views as children of the transitional container view so that they all appear during the animation.

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

The final part of the current transition is simply to animate the view, but we would like to, and then notify the transitionContext object if we have successfully completed the transition.

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

In SCOverlayDismissTransition will basically follow the same process, although is in the opposite direction.

Now, when we display the menu view controller, it will use our custom transition to keep the rendering view controller’s view in the view hierarchy.

The closing

As we approach the 6th anniversary of the iOS App Store, the App landscape is already breathtaking. The idea that we can already treat an app as a classic shows how fast it can move. Every year, developers get a new set of toys to build great applications, but there’s still plenty of room for UIScrollView.

You can check out the project on GitHub.

  1. If you’re feeling nostalgic for the mind-crushing iOS 6 days, there’s also a big clone of iOS 6 that pulls to refresh control on GitHub at ↩

Also, if you want to advance together, add a networking group1012951431, choose to join to communicate and learn together. Looking forward to your joining us! (Students can receive study package when entering the group)

Original address: subjc.com/unread-over…