原文地址在這里.原文
去年逢并,讀者們投票選出了Top5的iOS7最佳動(dòng)畫砍聊,當(dāng)然也很想看到有關(guān)這些動(dòng)畫如何實(shí)現(xiàn)的教程玻蝌。這次俯树,我們將會(huì)實(shí)現(xiàn)Taasky這個(gè)app的3D效果的側(cè)滑菜單许饿。
這篇教程比較適合開發(fā)經(jīng)驗(yàn)比較豐富的開發(fā)者。因?yàn)檫@篇教程涵蓋Autolayout秽晚,UIScrollView爆惧,viewcontroller容器還有CoreAnimation。這些對(duì)于初學(xué)者來說都比較陌生址遇,所以如果你之前沒有接觸過的話閱讀起來會(huì)有點(diǎn)困難倔约。
開始
首先下載一個(gè)我們的初始項(xiàng)目。地址在這里
下載之后打開他绢要,運(yùn)行起來重罪。
第一個(gè)頁面和點(diǎn)擊Cell之后進(jìn)入的第二個(gè)頁面是這樣的剿配。
第一個(gè)頁面是一個(gè)繼承自UITableViewController的Controller呼胚,名字叫做MenuViewController蝇更,從名字也能看出來了,這將會(huì)是我們的側(cè)滑菜單常遂。我們的TableView中使用的Cell是我們自定義的Cell克胳,叫做MenuItemCell漠另。每個(gè)Cell都是可以點(diǎn)擊的笆搓,點(diǎn)擊之后進(jìn)入的是另一個(gè)界面满败,叫做DetailViewController算墨,里面只有一張和點(diǎn)擊Cell匹配的同一種背景色和圖片净嘀。
例如點(diǎn)擊綠色的cell
現(xiàn)在這個(gè)app距離我們的完成形態(tài)還有不少距離暑刃。但是耐心跟著教程走是肯定可以完成的稍走。
首先我們需要按照下面幾個(gè)步驟來婿脸。
首先現(xiàn)在的app實(shí)際上是兩個(gè)頁面狐树,由navigationController來控制兩個(gè)controller的切換。我們第一步要做的就是利用Autolayout和viewcontroller container這兩個(gè)特性在塔,把這兩個(gè)viewcontroller合二為一放在一個(gè)容器里蛔溃,而這個(gè)容器我們會(huì)用scrollview來充當(dāng)贺待。
第二步是添加一個(gè)button來控制顯示和隱藏我們的菜單麸塞。
第三步實(shí)現(xiàn)我們菜單的3D化哪工,就像Taasky這個(gè)APP里面的菜單一樣得院。
最后一步,你要將菜單動(dòng)畫和scrollView的offset結(jié)合起來鸭限。
廢話不多說败京,我們新建一個(gè)Viewcontroller,用來當(dāng)做ViewController容器泛粹,名字就叫ContainerViewController.確保是繼承自UIViewController晶姊。語言選擇swift们衙。
同樣的在storyboard里也拉出一個(gè)ViewController,并把class改成我們的ContainerViewController忆蚀。Storyboard ID改成ContainerVC.
選擇view,并且把背景色改成黑色.
ok,拉一個(gè)UIScrollview到我們的view上.并且把垂直和水平滾動(dòng)條隱藏掉.把Delays Content Touches也取消掉.如圖.
右鍵單擊我們的scrollview,把delegate設(shè)置為我們的ContainerViewController.
給我們的scrollview添加約束.很簡(jiǎn)單的約束,上下左右與父view間距為0.
設(shè)置contentView
然后托一個(gè)view到我們的scrollview上,并且把size和背景色設(shè)置如圖的值.
把我們的view的Document Label設(shè)置為ContentView,用來和其他的view區(qū)別.
然后給我們的contentView添加約束.
然后把我們的Trailing這個(gè)約束的constant改為0.
這時(shí)候xcode會(huì)出現(xiàn)紅色的警告,是因?yàn)槲覀兊募s束沒有添加完成,因?yàn)槟闳绻唤oscrollview的contentview設(shè)置寬高的話,scrollview是沒辦法確定自己的contentsize的.
所以我們這樣設(shè)置.
把我們的ContentView的寬高設(shè)置為和ContainerViewController的view的寬高一致.
然后修改如下約束.
把constant改為80的意思就是,我們的Contentview的寬一直是底層view寬度+80(這80就是給我們的側(cè)邊欄準(zhǔn)備的.).
添加Menu和Detail Container Views
從storyboard找到一個(gè)叫做ContainerView的控件,相信這個(gè)控件很多人并沒有用過.這個(gè)控件就是在storyboard中為某個(gè)ViewController添加一個(gè)childViewController用的.
首先,拖一個(gè)ContainerView到我們的ContentView,寬高改為(80,600),然后Document里的label改為Menu Container View.
然后,再拖一個(gè)ContainerView到我們的ContentView,并且把size和Document里的label改為下圖所示的數(shù)值.
拖完之后我們的ContentView就會(huì)長(zhǎng)成這樣.
ContainerView有一個(gè)特性,就是你一旦拖出一個(gè)ContainerView,那么xcode會(huì)自動(dòng)幫你生成一個(gè)他的子ViewController.如圖.
顯然,系統(tǒng)幫我們生成的這兩個(gè)ViewController對(duì)我們來說是沒用的,因?yàn)槲覀円呀?jīng)有了MenuController和DetailController,所以刪掉他們.
刪掉之后,給我們的兩個(gè)ContainerView分別添加約束.Menu ContainerView的約束如下.
DetailContainerView的約束如下.
我們剛才刪除了系統(tǒng)幫我們生成的childViewController,現(xiàn)在我們需要手動(dòng)添加.
首先把我們的InitController改成我們的ContainerViewController.
然后右鍵點(diǎn)擊Menu ContainerView,拖一根線到我們的Navigation Controller.然后在彈出框中選擇embed.
一旦線拖好之后,我們的storyboard看起來是這樣子的.
肯定要改一改.首先把MenuController里的Cell里的UIImageView的width改成80.
然后,把MenuViewController和DetailViewController中間代表push的那個(gè)segue刪掉.
然后為我們的DetailViewController生成一個(gè)自己的navigationController.
選擇我們剛剛生成的navigationController,把我們的navagationbar改為如下.
然后把MenuViewController的navigationbar也改成一樣的參數(shù).并且把View Controller\Layout\Adjust Scroll View Insets選中.
ok,按照剛才拉MenuContainerView的方式拉一下DetailContainerView.
這樣,我們的ContainerViewController就擁有兩個(gè)childViewController了.
運(yùn)行一下.試試效果.
看起來不錯(cuò).但是有個(gè)問題.使勁往右拉的話,左邊會(huì)拉出來一片黑色的區(qū)域.這顯然不是我們想要的.
所以在Storyboard中找到我們的ScrollView.
1.選中Paging Enabled.
2.取消Bounce\Bounces的選中狀態(tài).
再運(yùn)行一次.向右拉,這次menu顯示正確了.不會(huì)在左邊漏出一大段黑色的空間.但是每次我們?cè)噲D隱藏menu的時(shí)候它又會(huì)彈回來.(實(shí)際上我按照教程做到這的時(shí)候并沒有發(fā)生這種情況,菜單是可以隱藏的.)
第二個(gè)問題是,點(diǎn)擊側(cè)邊欄,detailContainerView并不會(huì)發(fā)生變化.這很正常,因?yàn)槟氵€沒寫代碼呢.
修改我們的代碼
首先,把MenuViewController.swift里的這些代碼拷貝到我們的DetailViewController中.
overridefuncviewDidLoad(){super.viewDidLoad()// Remove the drop shadow from the navigation barnavigationController!.navigationBar.clipsToBounds =true}
這個(gè)的作用是消除navigationbar下面的一條特別細(xì)的線.
每次選擇一個(gè)MenuViewController里面的一個(gè)tableviewCell的時(shí)候,相應(yīng)的我們應(yīng)該設(shè)置DetailViewController里面的menuItem屬性.但是現(xiàn)在我們的MenuViewController和DetailViewController還沒有關(guān)聯(lián)起來.所以我們會(huì)利用ContainerViewController來建立兩個(gè)controller之間的聯(lián)系.
在ContainerViewController里添加這么一個(gè)屬性.
private var detailViewController: DetailViewController?
然后override我們的ContainerViewController里的prepareForSegue(_:sender:)方法.
overridefuncprepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?){ifsegue.identifier =="DetailViewSegue"{letnavigationController = segue.destinationViewControlleras!UINavigationControllerdetailViewController = navigationController.topViewControlleras?DetailViewController}}
別忘了設(shè)置我們的segue.identifier.如圖所示.
然后再添加一個(gè)menuItem的屬性到ContainerViewController里,并且監(jiān)聽如果menuItem被設(shè)置,那么讓detailViewController的menuItem相應(yīng)的也改變.
varmenuItem:NSDictionary? {didSet{ifletdetailViewController = detailViewController {? ? ? detailViewController.menuItem = menuItem? ? }? }}
然后,到我們的MenuViewController里,先刪除prepareForSegue這個(gè)方法,因?yàn)檫@個(gè)方法是以前MenuViewController和DetailViewController有直接關(guān)聯(lián)的時(shí)候才有用的,現(xiàn)在這個(gè)方法顯然已經(jīng)沒有意義了.
我們要做的就是在MenuViewController里的tableview 的delegate里添加以下的內(nèi)容.
// MARK: UITableViewDelegateoverridefunctableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){? tableView.deselectRowAtIndexPath(indexPath, animated:true)letmenuItem = menuItems[indexPath.row]as!NSDictionary(navigationController!.parentViewControlleras!ContainerViewController).menuItem = menuItem}
然后再在ViewDidLoad()方法里加入以下內(nèi)容,確保第一次進(jìn)入頁面的時(shí)候默認(rèn)選擇的是第一個(gè)Cell.
(navigationController!.parentViewController as! ContainerViewController).menuItem =
(menuItems[0] as! NSDictionary)
運(yùn)行一下.效果如下.
顯示和隱藏我們的Menu
現(xiàn)在我們點(diǎn)擊cell雖然DetailViewController的內(nèi)容可以正確顯示,但是菜單并不能自動(dòng)隱藏.所以我們首先要實(shí)現(xiàn)的是點(diǎn)擊菜單之后菜單自動(dòng)隱藏.
要實(shí)現(xiàn)這個(gè)效果,首先要把我們的ContainerViewController里的scrollView和MenuContainerView拖線拖到我們的ContainerViewController里.
如圖.
然后給ContainerViewController.swift添加一個(gè)新的方法.
hideOrShowMenu(_:animated:)
// MARK: ContainerViewControllerfunchideOrShowMenu(show: Bool, animated: Bool){letmenuOffset =CGRectGetWidth(menuContainerView.bounds)? scrollView.setContentOffset(show ?CGPointZero:CGPoint(x: menuOffset, y:0), animated: animated)}
然后在MenuItem的didSet里加入這個(gè)方法,意思就是每次設(shè)置menuItem的時(shí)候都會(huì)自動(dòng)調(diào)用這個(gè)方法.
overridefuncviewDidLoad(){super.viewDidLoad()? hideOrShowMenu(false, animated:false)}
運(yùn)行一下.
原文中提到了這時(shí)候菜單還是存在回彈和收不回去的問題,實(shí)際上在我做的時(shí)候并沒有出現(xiàn)這種情況.所以如果你們做的時(shí)候如果出現(xiàn)了回彈.那么需要在ContainerViewController里實(shí)現(xiàn)UIScrollView的這個(gè)Delegate.
// MARK: - UIScrollViewDelegatefuncscrollViewDidScroll(scrollView: UIScrollView){/*
Fix for the UIScrollView paging-related issue mentioned here:
http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top
*/scrollView.pagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width -CGRectGetWidth(scrollView.frame))}
然后運(yùn)行,這時(shí)候應(yīng)該沒問題了.
添加我們的漢堡按鈕
新建一個(gè)類繼承自UIView,起名叫做HamburgerView.swift.
然后修改內(nèi)容如下.
classHamburgerView:UIView{letimageView:UIImageView! =UIImageView(image:UIImage(named: “Hamburger”))? requiredinit(coder aDecoder:NSCoder) {super.init(coder: aDecoder)? ? configure()? }? requiredoverrideinit(frame:CGRect) {super.init(frame: frame)? ? configure()? }// MARK: Privateprivatefuncconfigure(){? ? imageView.contentMode =UIViewContentMode.CenteraddSubview(imageView)? }}
然后在我們的DetailViewController里,把他加進(jìn)去.先添加一個(gè)屬性
var hamburgerView: HamburgerView?
然后在viewDidLoad()里添加如下代碼.
let tapGestureRecognizer =UITapGestureRecognizer(target:self, action: “hamburgerViewTapped”)hamburgerView = HamburgerView(frame:CGRect(x:0, y:0, width:20, height:20))hamburgerView!.addGestureRecognizer(tapGestureRecognizer)navigationItem.leftBarButtonItem=UIBarButtonItem(customView: hamburgerView!)
這個(gè)手勢(shì)的事件hamburgerViewTapped()會(huì)調(diào)用 ContainerViewController’s hideOrShowMenu(_:animated:),但是現(xiàn)在缺少一個(gè)布爾值來表示菜單是否處于打開狀態(tài).所以我們?yōu)镃ontainerViewController添加一個(gè)布爾值用來記錄菜單的狀態(tài).
var showingMenu = false
然后override viewDidLayoutSubviews()方法.加入如下代碼.
overridefuncviewDidLayoutSubviews(){super.viewDidLayoutSubviews()? hideOrShowMenu(showingMenu, animated:false)}
這會(huì)在ContainerViewController的布局每次發(fā)生變化的時(shí)候調(diào)用hideorShow方法.
然后打開DetailViewController,添加我們的點(diǎn)擊事件.
funchamburgerViewTapped(){letnavigationController = parentViewControlleras!UINavigationControllerletcontainerViewController = navigationController.parentViewControlleras!ContainerViewControllercontainerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated:true)}
現(xiàn)在點(diǎn)擊漢堡按鈕,已經(jīng)能夠打開菜單了,但是再次點(diǎn)擊應(yīng)該是關(guān)閉菜單,然后并沒有效果,原因很簡(jiǎn)單,你沒有跟新showingMenu的值,所以在我們的hideOrShowMenu方法里加入showingMenu = show.
再試一下.
ok了.
然而,問題依然沒有結(jié)束.
當(dāng)你滑動(dòng)打開菜單的時(shí)候,需要點(diǎn)擊漢堡菜單兩次才能關(guān)閉菜單.這是因?yàn)槟慊瑒?dòng)打開菜單的時(shí)候并沒有更新showingMenu的值.所以,需要在UIScrollviewDelegate里更新我們的showingMenu.
funcscrollViewDidEndDecelerating(scrollView: UIScrollView){letmenuOffset =CGRectGetWidth(menuContainerView.bounds)? showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y:0), scrollView.contentOffset)println(“didEndDecelerating showingMenu \(showingMenu)”)}
運(yùn)行一下,注意一下console,當(dāng)你快速滑動(dòng)的時(shí)候是沒問題的,但是緩慢滑動(dòng)的時(shí)候這個(gè)方法似乎不響應(yīng).所以這個(gè)方法并不靠譜.
我們把代碼移到另一個(gè)代理方法scrollViewDidScroll(_:)里.
再次運(yùn)行.
應(yīng)該沒問題了.
給我們的菜單添加透視效果
實(shí)際上完整的效果華麗就華麗在菜單出現(xiàn)的方式并不是水平的,而是以3D旋轉(zhuǎn)的效果出現(xiàn)的.要實(shí)現(xiàn)這個(gè)效果我們必須計(jì)算菜單顯示的比例和菜單旋轉(zhuǎn)角度之間的關(guān)系.如下所示.
functransformForFraction(fraction:CGFloat)-> CATransform3D{? var identity = CATransform3DIdentity? identity.m34 = -1.0/1000.0;? let angle = Double(1.0- fraction) * -M_PI_2? let xOffset = CGRectGetWidth(menuContainerView.bounds) *0.5let rotateTransform = CATransform3DRotate(identity, CGFloat(angle),0.0,1.0,0.0)? let translateTransform = CATransform3DMakeTranslation(xOffset,0.0,0.0)returnCATransform3DConcat(rotateTransform, translateTransform)}
上面的方法就是計(jì)算菜單顯示的部分和旋轉(zhuǎn)角度的關(guān)系.
fraction當(dāng)menu完全隱藏的時(shí)候是0,完全顯示的時(shí)候是1.
CATransform3DIdentity代表原始的Transform.
CATransform3DIdentity’s m34這個(gè)值代表view的perspective.(設(shè)置了他旋轉(zhuǎn)的時(shí)候才會(huì)有3D效果)
利用CATransform3DRotate來實(shí)現(xiàn)菜單的旋轉(zhuǎn)效果.并且是繞Y軸旋轉(zhuǎn).-90度的時(shí)候代表與平面向內(nèi)垂直(所以你看不到).0度的時(shí)候水品展開.
translateTransform負(fù)責(zé)menu在旋轉(zhuǎn)的時(shí)候同時(shí)位移到正確的位置.
CATransform3DConcat負(fù)責(zé)把位置的transform和旋轉(zhuǎn)的transform結(jié)合起來.
現(xiàn)在在我們的scrollViewDidScroll這個(gè)代理方法里加入以下代碼.
letmultiplier =1.0/ CGRectGetWidth(menuContainerView.bounds)letoffset = scrollView.contentOffset.x * multiplierletfraction =1.0- offsetmenuContainerView.layer.transform = transformForFraction(fraction)menuContainerView.alpha = fraction
運(yùn)行一下.
效果似乎不太對(duì).那是因?yàn)槲覀儾]有設(shè)置menuContainerView的anchorpoint,現(xiàn)在的anchorPoint還是在view的中心點(diǎn)我們實(shí)際上的anchorpoint應(yīng)該是在view中心最右的位置.所以在ContainerViewController的viewDidLayoutSubViews()里修改anchorPoint.
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
運(yùn)行.
效果不錯(cuò).
讓漢堡按鈕動(dòng)起來.
我們只剩最后一個(gè)效果了,就是菜單出現(xiàn)的過程中,漢堡按鈕也要轉(zhuǎn)相應(yīng)的角度.
在HamburgerView中添加下面的方法.
funcrotate(fraction: CGFloat){letangle =Double(fraction) *M_PI_2imageView.transform =CGAffineTransformMakeRotation(CGFloat(angle))}
然后在ContainerViewController里的scrollViewDidScroll()里添加以下代碼.
ifletdetailViewController = detailViewController {ifletrotatingView = detailViewController.hamburgerView {? ? rotatingView.rotate(fraction)? }}
運(yùn)行一下.
Perfect!
從這里獲取最終的程序.
如果你對(duì)perspective有疑問.那么請(qǐng)?jiān)谶@里瀏覽相關(guān)信息.
有任何疑問可以留言.
如果你認(rèn)為這篇文章不錯(cuò),也有閑錢抓半,那你可以用支付寶掃描下方二維碼隨便捐助一點(diǎn)琅关,以慰勞作者的辛苦
文/葉孤城___(簡(jiǎn)書作者)
原文鏈接:http://www.reibang.com/p/a7f5cab17395
著作權(quán)歸作者所有涣易,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)新症,并標(biāo)注“簡(jiǎn)書作者”响禽。