乐趣区

iOS开发高级分享-Unread的下拉式选单

解构革命的演变

背景

2013 年中期,RSS 世界遭受了沉重打击。谷歌宣布,他们()RSS 订阅服务,谷歌阅读器,是被关闭了。有了它,数以百万计的声音突然惊恐地大叫,并突然保持沉默。

使用量下降是关闭的主要原因,尽管来自 Google Reader 用户的巨大反应表明,该服务仍在吸引大量用户。网络上充满了对 RSS 和整个开放网络的未来的担忧,尽管也有一种乐观的感觉,那些没有像 Google 这样的巨人资源的人有机会在曾经的真空中立足一个紧密控制的市场。为 Google Reader 流亡者打造有价值的替代品。

尽管可能是丧钟之声,但 RSS 仍然活跃并且今天很好,诸如 Feedly,Feedwrangler 和 Feedbin 之类的服务填补了 Google Reader 灭亡所留下的空白。随之而来的是新型的现代 iOS RSS 阅读器。其中之一是 Unread,这是由 Jared Sinclair 建立的,提供上述服务的令人愉快的干净易用客户端。在应用商店中花费的时间很短,它已经吸引了相当多的关注者,以至于有很大的机会让您阅读 Unread 的这些话。

本文介绍的是 Unread 的菜单交互功能,但也涉及历史,我们走了多远以及如何到达这里。

景观

如果要在 iOS 上绘制新闻和内容聚合应用程序的格局,则可以在比例尺的一端绘制 Flipboard 和 Pulse(现在为 LinkedIn Pulse)之类的应用程序,其中体验不仅会推动内容消费,还会推动内容发现。这些是您想象中的应用程序,当您在周日的早晨坐下来喝咖啡(对付那些茶的人)而迷失在杂志体验中时,便会使用这些应用程序。

相反,我们拥有 Reeder 之类的应用程序,这些应用程序将以最有效的方式消费内容,而您用来逃避日常通勤单调或摆脱 FOMO 的应用程序。这是可能绘制未读的地方。

未读继续我们讨论的克制主题之前。它本身的计费方式很简单:您登录到所选的 RSS 聚合帐户并阅读。而已。在这种斯巴达精神中,“未读”提供了为单手使用而设计和制造的体验。

要真正了解 Unread 菜单交互的来源,让我们了解一下 Darwinistic。

演化

如果我们回顾一下被视为 iOS 开发图标的应用程序 Tweetie,它就向我们介绍了现在司空见惯的“按需刷新”模式。Pull-to-refresh 变得如此被接受,甚至可以预期,它已被 Apple 验证,并被用作刷新 Mail.app 收件箱的默认机制。

然后是 Facebook iOS 应用程序,该应用程序使导航抽屉(又名“上帝汉堡”,“汉堡地下室”和许多其他上流社会)得到了普及。自从他们在导航中删除了它(对于联系人仍然保留)之后,它在整个 iOS 设计环境中的传播程度使其成为一种常规的可接受模式。

快进到今天,我们有了 Unread 的菜单,这是两种公认的传统模式的混合物。这是两次革命性互动的发展,为我们如何与设备互动开创了先例。

Unread 提供了有关首次启动的教程,该教程说明了如何显示菜单,尽管有人可能会认为不需要菜单。它是其血统的产物,因此,可以依靠该血统已经建立的一定程度的期望和理解。

解构

今年的 WWDC 为开发人员带来了许多 新的亮点:UIKit Dynamics,T​​ext Kit,Sprite Kit 和 UIViewController 过渡仅举几例。我们将使用其中的两个来重新创建 Unread 的菜单,即 UIViewController 过渡和 UIKit Dynamics,尽管后者我们将不直接处理。

拉动内容以显示菜单时,我们注意到的第一件事是拉动指示器中的弹簧。强烈对比的重点,低调的阅读界面未读很难错过。这让人想起了(短暂的)iOS 6 短暂刷新动画1,令人愉悦地描述了交互过程。

过去,我们已经介绍过类似的动态行为,并使用 UIKit Dynamics 对其进行了实现,这一次,我们将加强一个抽象层。

那 7 参数法

Objective- C 的一大功能就是命名参数。加上语言的冗长性,它提供了一种自然的方式来描述和记录方法的意图,尽管某些方法的长度可能会吓到一些新开发人员。一种这样的方法是新添加的基于 UIView 块的动画方法,animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:该方法虽然不是 Cocoa Touch 中最长的方法,但肯定在记分板上。

尽管存在强大的功能,但是它是一种非常简单易用但功能强大的方法,用于向界面添加动态动画行为,而无需提取完整的 UIKit Dynamics 堆栈。一些细心的 读者注意到,以前的帖子中的动态行为可能已使用此方法实现,因此,将其用于按菜单换行的弹簧行为似乎是一个很好的机会。

Stretttch

如果您想将橡皮筋拉长,则橡皮筋伸展得越深,橡皮筋就会变得越薄。这种物理行为反映在 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:

我们已经看到了最前,这种方法使用的参数,但让我们快速浏览一下这两个事关对我们来说,usingSpringWithDampinginitialSpringVelocity

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,我们需要创建一个包含三个SCSpringExpandingViews 的视图。叫它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: 方法时,我们将需要更新它们。如果回头看未读,我们可以看到每个弹簧视图的三个不同状态:折叠,展开和完成。我们已经提到了前两个,但最后一个是指示“菜单拉动”交互已经达到释放点将触发显示菜单的点。

为了实现这一点,我们将遍历三个 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++;
    }
}

现在,我们已经涵盖了一些新的热点,让我们绕过一条人迹罕至的道路。

嵌套的 UIScrollView

问任何 iOS 开发者,他们会告诉你,嵌套的滚动视图 用户界面元素,以至于苹果已经专门有一章他们的 UIScrollView 节目指南的话题。我们一起研究了这么多创新的 iOS 界面而没有提及它们是犯罪行为。

对于我们的示例内容,我们将通过展示一些 UITextView 吸引人的 Lorem Ipsum,该类在 iOS 7 的全新改版中获得了一些 TextKit 的喜爱。尽管我们不会在此条目中涵盖任何新的 API,但有兴趣的人应该查看 objc.io 上的精彩文章。相反,我们只需要记住那 UITextView 是强大的子类UIScrollView

我们希望我们 SCDragAffordanceView 始终在您身边,准备展示我们的菜单。要考虑的一个选择是将其添加为我们的子视图 UITextView 基础上,并修改其垂直原点 contentOffset 我们的 UITextView,但这种重载我们的责任UITextView 不仅仅是显示文本,只是 感觉 有点不对劲。

相反,让我们创建一个单独的实例 UIScrollView,我们的UITextViewSCDragAffordanceView将被添加为的子视图。

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

此处的关键行设置 alwaysBounceHorizontalYES。现在,无论 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 热点问题。

UIViewControllerTransitioningDelegate

版本有什么不同。如果这篇文章是在 iOS 7 之前编写的,那将是一件漫长而需要注意的事情。以前,如果您希望使用“未读”的下拉菜单等行为,则必须将视图插入当前视图控制器,窗口或其他类似臭味的行为之上。虽然这将为您带来理想的效果,但总感觉好像您违反了框架的要求。

值得庆幸的是,在 iOS 7 中,Apple 注意到了这种模式的出现,并从开发人员社区得到了另一个提示,它提供了一种干净,经过批准的方法,可以使用一组最少的协议来实现这一目标。现在,您可以通过实现 UIViewControllerTransitioningDelegate 协议来定义自定义动画和视图控制器之间的交互式过渡。

UIViewControllerTransitioningDelegate 协议声明了一些方法,这些方法使您可以返回动画师对象,这些对象定义了视图过渡的三个阶段之一:呈现,关闭和交互。我们的自定义过渡将定义展示和发布阶段。

在我们的视图控制器中,我们将声明我们遵守 UIViewControllerTransitioningDelegate 协议并实现我们关心的两种方法 animationControllerForPresentedController:presentingController:sourceController:animationControllerForDismissedController:

现在,我们为自定义视图控制器过渡提供了回调,我们需要一个视图控制器来呈现。未读的整洁菜单项动画不在本文讨论范围之内,因此对于我们而言,我们只需要创建一个视图控制器(SCMenuViewController),即可在触发菜单交互时显示该视图控制器。

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

创建此类的实例后,我们需要将其 transitionDelegate 设置为我们的视图控制器,并将其设置为 modalPresentationStyleUIModalPresentationCustom 以便 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 上签出该项目。

  1. 如果你感到怀旧的令人心醉的 iOS 6 天,还有的 iOS 6 中拉来刷新控制的一大克隆 GitHub 上 ↩

iOS 开发高级分享 – 兼容暗模式

iOS 开发高级分享 – 多种 Cell 高度自适应实现方案的 UI 流畅度分析

iOS 开发高级分享 – 针对对 Masonry 下的 FPS 优化讨论

iOS 开发高级分享 – App 间账号共享与 SDK 封装

iOS 开发高级分享 – MacOSCatalina 和 Xcode 11 的快速​UI 预览

翻译地址:http://subjc.com/unread-overlay-menu#article

退出移动版