iOS開發高級分享 - Unread的下拉式選單

解構革命的演變

背景

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,T​​ext 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對其進行了實現,這一次,咱們將增強一個抽象層。

那7參數法

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/)中的動態行爲可能已使用此方法實現,所以,將其用於按菜單換行的彈簧行爲彷佛是一個很好的機會。

Stretttch

若是您想將橡皮筋拉長,則橡皮筋伸展得越深,橡皮筋就會變得越薄。這種物理行爲反映在[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++;
}
}

 

如今,咱們已經涵蓋了一些新的熱點,讓咱們繞過一條人跡罕至的道路。

嵌套的UIScrollView

問任何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熱點問題。

UIViewControllerTransitioningDelegate

版本有什麼不一樣。若是這篇文章是在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]

相關文章
相關標籤/搜索