2013年中期,RSS世界遭受了沉重打击。谷歌宣布,他们(*的*)RSS订阅服务,[谷歌阅读器],是被关闭了。有了它,数以百万计的声音忽然惊恐地大叫,并忽然保持沉默。html
使用量降低是关闭的主要缘由,尽管来自[Google Reader]用户的巨大反应代表,该服务仍在吸引大量用户。网络上充满了对RSS和整个开放网络的将来的担心,尽管也有一种乐观的感受,那些没有像Google这样的巨人资源的人有机会在曾经的真空中立足一个紧密控制的市场。为[Google Reader]流亡者打造有价值的替代品。ios
尽管多是丧钟之声,但RSS仍然活跃而且今天很好,诸如[Feedly],[Feedwrangler]和[Feedbin之类的服务]填补了[Google Reader]灭亡所留下的空白。随之而来的是新型的现代iOS RSS阅读器。其中之一是[Unread],这是由[Jared Sinclair]创建的,提供上述服务的使人愉快的干净易用客户端。在应用商店中花费的时间很短,它已经吸引了至关多的关注者,以致于有很大的机会让您阅读[Unread的](http://jaredsinclair.com/unread/)这些话。git
本文介绍的是[Unread]的菜单交互功能,但也涉及历史,咱们走了多远以及如何到达这里。github
若是要在iOS上绘制新闻和内容聚合应用程序的格局,则能够在比例尺的一端绘制[Flipboard]和[Pulse](如今为LinkedIn Pulse)之类的应用程序,其中体验不只会推进内容消费,还会推进内容发现。这些是您想象中的应用程序,当您在周日的早晨坐下来喝咖啡(对付那些茶的人)而迷失在杂志体验中时,便会使用这些应用程序。spring
相反,咱们拥有[Reeder之类的]应用程序,这些应用程序将以最有效的方式消费内容,而您用来逃避平常通勤单调或摆脱[FOMO的应用程序]。这是可能绘制[未读的地方]。windows
[未读]继续咱们讨论的克制主题[以前]。它自己的计费方式很简单:您登陆到所选的RSS聚合账户并阅读。而已。在这种斯巴达精神中,“ [未读”]提供了为单手使用而设计和制造的体验。网络
要真正了解[Unread](http://jaredsinclair.com/unread/)菜单交互的来源,让咱们了解一下Darwinistic。app
若是咱们回顾一下被视为iOS开发图标的应用程序[Tweetie](http://en.wikipedia.org/wiki/Tweetie),它就向咱们介绍了如今司空见惯的“按需刷新”模式。Pull-to-refresh变得如此被接受,甚至能够预期,它已被Apple验证,并被用做刷新Mail.app收件箱的默认机制。框架
而后是[Facebook iOS应用程序](https://itunes.apple.com/au/app/facebook/id284882215?mt=8%E2%80%8E),该[应用程序](https://itunes.apple.com/au/app/facebook/id284882215?mt=8%E2%80%8E)使导航抽屉(又名“上帝汉堡”,“汉堡地下室”和许多其余上流社会)获得了普及。自从他们在导航中删除了它(对于联系人仍然保留)以后,它在整个iOS设计环境中的传播程度使其成为一种常规的可接受模式。布局
快进到今天,咱们有了[Unread](http://jaredsinclair.com/unread/)的菜单,这是两种公认的传统模式的混合物。这是两次革命性互动的发展,为咱们如何与设备互动开创了先例。
[Unread](http://jaredsinclair.com/unread/)提供了有关首次启动的教程,该教程说明了如何显示菜单,尽管有人可能会认为不须要菜单。它是其血统的产物,所以,能够依靠该血统已经创建的必定程度的指望和理解。
今年的WWDC 为开发人员带来了许多*新的亮点*:UIKit Dynamics,Text Kit,Sprite Kit和UIViewController过渡仅举几例。咱们将使用其中的两个来从新建立[Unread](http://jaredsinclair.com/unread/)的菜单,即UIViewController过渡和UIKit Dynamics,尽管后者咱们将不直接处理。
拉动内容以显示菜单时,咱们注意到的第一件事是拉动指示器中的弹簧。强烈对比的重点,低调的阅读界面[未读](http://jaredsinclair.com/unread/)很难错过。这让人想起了(短暂的)iOS 6短暂刷新动画<sup>[1](http://subjc.com/unread-overlay-menu#fn:1)</sup>,使人愉悦地描述了交互过程。
[过去](http://subjc.com/castro-playback-scrubber/),咱们已经介绍过相似的动态行为[,](http://subjc.com/castro-playback-scrubber/)并使用UIKit Dynamics对其进行了实现,这一次,咱们将增强一个抽象层。
Objective-C的一大功能就是命名参数。加上语言的冗长性,它提供了一种天然的方式来描述和记录方法的意图,尽管某些方法的长度可能会吓到一些新开发人员。一种这样的方法是新添加的基于UIView块的动画方法,`animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:`该方法虽然不是Cocoa Touch中最长的方法,但确定在[记分板上](https://github.com/Quotation/LongestCocoa)。
尽管存在强大的功能,可是它是一种很是简单易用但功能强大的方法,用于向界面添加动态动画行为,而无需提取完整的UIKit Dynamics堆栈。一些[细心的](http://iosdevweekly.com/issues/136) [读者](https://twitter.com/followben/status/442224305657483264)注意到,[之前的帖子](http://subjc.com/castro-playback-scrubber/)中的动态行为可能已使用此方法实现,所以,将其用于按菜单换行的弹簧行为彷佛是一个很好的机会。
若是您想将橡皮筋拉长,则橡皮筋伸展得越深,橡皮筋就会变得越薄。这种物理行为反映在[Unread](http://jaredsinclair.com/unread/)的拉动交互中,虽然它是一个很小的细节,但除非您正在寻找,不然您可能不会注意到,它加强了一种感受,即当咱们将滚动视图拖动到其上方时`contentSize`,咱们遭到抵抗。
为了在咱们的实现中模仿这种行为,咱们将提供一个view(`SCSpringExpandingView`),以在两个不一样的帧之间进行动画处理。折叠,未展开状态的视图框架将占据其父视图的整个宽度,而且高度匹配,从而为咱们提供一个小的正方形视图。
- (CGRect)frameForCollapsedState { return CGRectMake(0.f, CGRectGetMidY(self.bounds) - (CGRectGetWidth(self.bounds) / 2.f), CGRectGetWidth(self.bounds), CGRectGetWidth(self.bounds)); }
当咱们将视图拉伸到展开状态时,咱们将使用一个框架,该框架是超级视图的高度,但只有宽度的一半。咱们还将移动水平原点,以使咱们的视图保持在超级视图的中心内。
- (CGRect)frameForExpandedState { return CGRectMake(CGRectGetWidth(self.bounds) / 4.f, 0.f, CGRectGetWidth(self.bounds) / 2.f, CGRectGetHeight(self.bounds)); }
为了使视图的角变圆,咱们将`cornerRadius`拉伸视图的图层的层设置为视图宽度的一半,使其在折叠时呈现圆形外观,在扩展时呈现圆形边缘。在修改框架的宽度时,咱们须要在折叠状态和展开状态之间转换时更新此值,不然其中一种状况的边缘将变成圆角,这与视图的宽度相反。
- (void)layoutSubviews { [super layoutSubviews]; self.stretchingView.layer.cornerRadius = CGRectGetMidX(self.stretchingView.bounds); }
如今剩下要作的就是使用咱们的新朋友长名来在两个州之间创建动画`animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:`。
咱们已经看到了最前,这种方法使用的参数,但让咱们快速浏览一下这两个事关对咱们来讲,`usingSpringWithDamping`和`initialSpringVelocity`。
`usingSpringWithDamping``CGFloat`从0.0到1.0之间取一个值,并从物理意义上肯定弹簧的强度。接近1.0的值将增长弹簧的强度并致使低振动。接近0.0的值会削弱弹簧的强度并致使高振动。
`initialSpringVelocity`也要接受,`CGFloat`可是传递的值将相对于动画过程当中通过的距离。值1.0表示在1秒钟内遍历的动画距离,而值0.5表示在1秒钟内遍历的动画距离的一半。
尽管这些参数与物理属性相对应,但在大多数状况下*仍是感受良好,请这样作*。
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.4f initialSpringVelocity:0.5f options:UIViewAnimationOptionBeginFromCurrentState animations:^{ self.stretchingView.frame = [self frameForExpandedState]; } completion:NULL];
就是这样。只需一个方法调用和一些波动的魔术数字,咱们就能够利用iOS 7中UIKit的动态基础。
如今,咱们已经建立了`SCSpringExpandingView`,咱们须要建立一个包含三个`SCSpringExpandingView`s 的视图。叫它`SCDragAffordanceView`。
的基本工做`SCDragAffordanceView`是布局三个`SCSpringExpandingView`,并提供一个接口,咱们能够经过该接口进行下拉菜单交互。
要布局`SCSpringExpandingView`,咱们将覆盖`layoutSubviews`并对齐每一个视图框架,使其在边界上等距分布。
- (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++; } }
如今咱们已经布局了视图,当有人调用该`setProgress:`方法时,咱们将须要更新它们。若是回头看[未读](http://jaredsinclair.com/unread/),咱们能够看到每一个弹簧视图的三个不一样状态:折叠,展开和完成。咱们已经提到了前两个,但最后一个是指示“菜单拉动”交互已经达到释放点将触发显示菜单的点。
为了实现这一点,咱们将遍历三个`SCSpringExpandingView`s并主要基于`progress`传入的是大于仍是等于1.0来更新每一个s的颜色,而后基于s 是否`progress`足够大以使视图能够扩展。
- (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++; } }
如今,咱们已经涵盖了一些新的热点,让咱们绕过一条人迹罕至的道路。
问任何iOS开发者,他们会告诉你,嵌套的滚动视图*的*用户界面元素,以致于苹果已经[专门有一章](https://developer.apple.com/library/ios/documentation/windowsviews/conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html)他们的`UIScrollView`节目指南的话题。咱们一块儿研究了这么多创新的iOS界面而没有说起它们是犯罪行为。
对于咱们的示例内容,咱们将经过展现一些`UITextView`吸引人的Lorem Ipsum ,该类在iOS 7的全新改版中得到了一些TextKit的喜好。尽管咱们不会在此条目中涵盖任何新的API,但有兴趣的人应该查看[objc.io上](http://www.objc.io/issue-5/getting-to-know-textkit.html)的[精彩文章](http://www.objc.io/issue-5/getting-to-know-textkit.html)。相反,咱们只须要记住那`UITextView`是强大的子类`UIScrollView`。
咱们但愿咱们`SCDragAffordanceView`始终在您身边,准备展现咱们的菜单。要考虑的一个选择是将其添加为咱们的子视图`UITextView`基础上,并修改其垂直原点`contentOffset`咱们的`UITextView`,但这种重载咱们的责任`UITextView`不只仅是显示文本,只是*感受*有点不对劲。
相反,让咱们建立一个单独的实例`UIScrollView`,咱们的`UITextView`和`SCDragAffordanceView`将被添加为的子视图。
self.enclosingScrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; self.enclosingScrollView.alwaysBounceHorizontal = YES; self.enclosingScrollView.delegate = self; [self.view addSubview:self.enclosingScrollView];
此处的关键行设置`alwaysBounceHorizontal`为`YES`。如今,不管`contentSize`滚动视图如何,水平拖动始终将以预期的阻力继续超出界限。
若是咱们嵌套`UITextView`的水平内容大小没有超出其范围,那么咱们将得到仅一个的效果`UIScrollView`,同时在代码中分离关注点。
咱们还但愿成为滚动视图的委托,以便咱们检测到滚动视图被拖动并相应地更新`SCDragAffordanceView`的进度。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView.isDragging) { self.menuDragAffordanceView.progress = scrollView.contentOffset.x / CGRectGetWidth(self.menuDragAffordanceView.bounds); } }
最后,当咱们收到`scrollViewDidEndDragging:willDecelerate:`委托回调时,咱们将使用在`scrollViewDidScroll:`回调中计算出的相同进度来肯定是否显示菜单视图控制器。若是没有,咱们将进度设置回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; } }
有了尘土飞扬的道路,让咱们陷入下一个iOS 7热点问题。
版本有什么不一样。若是这篇文章是在iOS 7以前编写的,那将是一件漫长而须要注意的事情。之前,若是您但愿使用“ [未读](http://jaredsinclair.com/unread/) ”的下拉菜单等行为,则必须将视图插入当前视图控制器,窗口或其余相似臭味的行为之上。虽然这将为您带来理想的效果,但总感受好像您违反了框架的要求。
值得庆幸的是,在iOS 7中,Apple注意到了这种模式的出现,并从开发人员社区获得了另外一个提示,它提供了一种干净,通过批准的方法,可使用一组最少的协议来实现这一目标。如今,您能够经过实现`UIViewControllerTransitioningDelegate`协议来定义自定义动画和视图控制器之间的交互式过渡。
该`UIViewControllerTransitioningDelegate`协议声明了一些方法,这些方法使您能够返回动画师对象,这些对象定义了视图过渡的三个阶段之一:呈现,关闭和交互。咱们的自定义过渡将定义展现和发布阶段。
在咱们的视图控制器中,咱们将声明咱们遵照`UIViewControllerTransitioningDelegate`协议并实现咱们关心的两种方法`animationControllerForPresentedController:presentingController:sourceController:`和`animationControllerForDismissedController:`。
如今,咱们为自定义视图控制器过渡提供了回调,咱们须要一个视图控制器来呈现。[未读](http://jaredsinclair.com/unread/)的整洁菜单项动画不在本文讨论范围以内,所以对于咱们而言,咱们只须要建立一个视图控制器(`SCMenuViewController`),便可在触发菜单交互时显示该视图控制器。
self.menuViewController = [[SCMenuViewController alloc] initWithNibName:nil bundle:nil];
建立此类的实例后,咱们须要将其`transitionDelegate`设置为咱们的视图控制器,并将其设置为`modalPresentationStyle`,`UIModalPresentationCustom`以便`transitioningDelegate`在出现时能够回调它。
self.menuViewController.modalPresentationStyle = UIModalPresentationCustom;
self.menuViewController.transitioningDelegate = self;
如今,当咱们展现菜单视图控制器时,它将回调到其`transitioningDelegate`(咱们的视图控制器)以请求展现`UIViewControllerAnimatedTransitioning`动画器对象。
## UIViewControllerAnimatedTransitioning
为了向菜单视图控制器提供动画对象,咱们将从建立一个普通的旧NSObject子类开始`SCOverlayPresentTransition`,并声明其符合`UIViewControllerAnimatedTransitioning`协议。在`animationControllerForPresentedController:presentingController:sourceController:`委托回调中,咱们将建立`SCOverlayPresentTransition`对象的实例并返回它。
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return [[SCOverlayPresentTransition alloc] init]; }
对于解雇动画,咱们将建立另外一个名为NSObject的子类`SCOverlayDismissTransition`,并在收到`animationControllerForDismissedController:`委托回调时提供其实例。
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return [[SCOverlayDismissTransition alloc] init]; }
咱们如今和罢免过渡对象的实现包括两种方法,`transitionDuration:`和`animateTransition:`。`transitionDuration:`您可能已经猜到的方法只是请求`NSTimeInterval`来指定动画的持续时间。该`animateTransition:`是在过渡的实质性工做。
该`animateTransition:`方法的惟一参数是符合`UIViewControllerContextTransitioning`协议的对象。从该对象中,咱们能够提取驱动动画所需的对象和信息,包括过渡中涉及的视图控制器。它还提供了一些方法,用于通知框架咱们已完成过渡。
UIViewController *presentingViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *overlayViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
一旦有了呈现和呈现的视图控制器,就须要将它们的视图添加为过渡的容器视图的子视图,以便它们都在动画期间出现。
UIView *containerView = [transitionContext containerView];
[containerView addSubview:presentingViewController.view];
[containerView addSubview:overlayViewController.view];
当前过渡的最后一部分是简单地为视图设置动画,可是咱们愿意,而后通知`transitionContext`对象咱们是否已成功完成过渡。
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]; }];
在`SCOverlayDismissTransition`将遵循基本上相同的过程,尽管是在相反的方向。
如今,当咱们显示菜单视图控制器时,它将使用咱们的自定义过渡,将呈现视图控制器的视图保持在视图层次结构中。
当咱们即将迎来iOS App Store成立6周年之际,其应用前景已使人叹为观止。咱们已经能够将应用视为经典的想法代表了它的移动速度。每一年,开发人员都会得到一系列新的玩具,以用来构建出色的应用程序,但仍然有可观的空间`UIScrollView`。
您能够[在GitHub上签出该项目](https://github.com/subjc/SubjectiveCUnreadMenu)。
1. 若是你感到怀旧的使人心醉的iOS 6天,还有的iOS 6中拉来刷新控制的一大克隆[GitHub上](https://github.com/Sephiroth87/ODRefreshControl) [↩](http://subjc.com/unread-overlay-menu#fnref:1)
另外,若是你想一块儿进阶,不妨添加一下交流群[1012951431],选择加入一块儿交流,一块儿学习。期待你的加入!(进群可领取学习礼包)
翻译地址:[http://subjc.com/unread-overlay-menu#article]