原文地址在這裡.原文
去年,讀者們投票選出了Top5的iOS7最佳動畫,當然也很想看到有關這些動畫如何實現的教程。這次,我們將會實現Taasky這個app的3D效果的側滑菜單。
這篇教程比較適合開發經驗比較豐富的開發者。因為這篇教程涵蓋Autolayout,UIScrollView,viewcontroller容器還有CoreAnimation。這些對於初學者來說都比較陌生,所以如果你之前沒有接觸過的話閱讀起來會有點困難。
開始
首先下載一個我們的初始項目。地址在這裡
下載之後打開他,運行起來。
第一個頁面和點擊Cell之後進入的第二個頁面是這樣的。
第一個頁面是一個繼承自UITableViewController的Controller,名字叫做MenuViewController,從名字也能看出來了,這將會是我們的側滑菜單。我們的TableView中使用的Cell是我們自定義的Cell,叫做MenuItemCell。每個Cell都是可以點擊的,點擊之後進入的是另一個界面,叫做DetailViewController,裡面只有一張和點擊Cell匹配的同一種背景色和圖片。
例如點擊綠色的cell
現在這個app距離我們的完成形態還有不少距離。但是耐心跟著教程走是肯定可以完成的。
首先我們需要按照下面幾個步驟來。
首先現在的app實際上是兩個頁面,由navigationController來控制兩個controller的切換。我們第一步要做的就是利用Autolayout和viewcontroller container這兩個特性,把這兩個viewcontroller合二為一放在一個容器裡,而這個容器我們會用scrollview來充當。
第二步是添加一個button來控制顯示和隱藏我們的菜單。
第三步實現我們菜單的3D化,就像Taasky這個APP裡面的菜單一樣。
最後一步,你要將菜單動畫和scrollView的offset結合起來。
廢話不多說,我們新建一個Viewcontroller,用來當做ViewController容器,名字就叫ContainerViewController.確保是繼承自UIViewController。語言選擇swift。
同樣的在storyboard裡也拉出一個ViewController,並把class改成我們的ContainerViewController。Storyboard ID改成ContainerVC.
選擇view,並且把背景色改成黑色.
ok,拉一個UIScrollview到我們的view上.並且把垂直和水平滾動條隱藏掉.把Delays Content Touches也取消掉.如圖.
右鍵單擊我們的scrollview,把delegate設置為我們的ContainerViewController.
給我們的scrollview添加約束.很簡單的約束,上下左右與父view間距為0.
設置contentView
然後托一個view到我們的scrollview上,並且把size和背景色設置如圖的值.
把我們的view的Document Label設置為ContentView,用來和其他的view區別.
然後給我們的contentView添加約束.
然後把我們的Trailing這個約束的constant改為0.
這時候Xcode會出現紅色的警告,是因為我們的約束沒有添加完成,因為你如果不給scrollview的contentview設置寬高的話,scrollview是沒辦法確定自己的contentsize的.
所以我們這樣設置.
把我們的ContentView的寬高設置為和ContainerViewController的view的寬高一致.
然後修改如下約束.
把constant改為80的意思就是,我們的Contentview的寬一直是底層view寬度+80(這80就是給我們的側邊欄准備的.).
添加Menu和Detail Container Views
從storyboard找到一個叫做ContainerView的控件,相信這個控件很多人並沒有用過.這個控件就是在storyboard中為某個ViewController添加一個childViewController用的.
首先,拖一個ContainerView到我們的ContentView,寬高改為(80,600),然後Document裡的label改為Menu Container View.
然後,再拖一個ContainerView到我們的ContentView,並且把size和Document裡的label改為下圖所示的數值.
拖完之後我們的ContentView就會長成這樣.
ContainerView有一個特性,就是你一旦拖出一個ContainerView,那麼xcode會自動幫你生成一個他的子ViewController.如圖.
顯然,系統幫我們生成的這兩個ViewController對我們來說是沒用的,因為我們已經有了MenuController和DetailController,所以刪掉他們.
刪掉之後,給我們的兩個ContainerView分別添加約束.Menu ContainerView的約束如下.
DetailContainerView的約束如下.
我們剛才刪除了系統幫我們生成的childViewController,現在我們需要手動添加.
首先把我們的InitController改成我們的ContainerViewController.
然後右鍵點擊Menu ContainerView,拖一根線到我們的Navigation Controller.然後在彈出框中選擇embed.
一旦線拖好之後,我們的storyboard看起來是這樣子的.
肯定要改一改.首先把MenuController裡的Cell裡的UIImageView的width改成80.
然後,把MenuViewController和DetailViewController中間代表push的那個segue刪掉.
然後為我們的DetailViewController生成一個自己的navigationController.
選擇我們剛剛生成的navigationController,把我們的navagationbar改為如下.
然後把MenuViewController的navigationbar也改成一樣的參數.並且把View Controller\Layout\Adjust Scroll View Insets選中.
ok,按照剛才拉MenuContainerView的方式拉一下DetailContainerView.
這樣,我們的ContainerViewController就擁有兩個childViewController了.
運行一下.試試效果.
看起來不錯.但是有個問題.使勁往右拉的話,左邊會拉出來一片黑色的區域.這顯然不是我們想要的.
所以在Storyboard中找到我們的ScrollView.
1.選中Paging Enabled.
2.取消Bounce\Bounces的選中狀態.
再運行一次.向右拉,這次menu顯示正確了.不會在左邊漏出一大段黑色的空間.但是每次我們試圖隱藏menu的時候它又會彈回來.(實際上我按照教程做到這的時候並沒有發生這種情況,菜單是可以隱藏的.)
第二個問題是,點擊側邊欄,detailContainerView並不會發生變化.這很正常,因為你還沒寫代碼呢.
修改我們的代碼
首先,把MenuViewController.swift裡的這些代碼拷貝到我們的DetailViewController中.
override func viewDidLoad() { super.viewDidLoad() // Remove the drop shadow from the navigation bar navigationController!.navigationBar.clipsToBounds = true }
這個的作用是消除navigationbar下面的一條特別細的線.
每次選擇一個MenuViewController裡面的一個tableviewCell的時候,相應的我們應該設置DetailViewController裡面的menuItem屬性.但是現在我們的MenuViewController和DetailViewController還沒有關聯起來.所以我們會利用ContainerViewController來建立兩個controller之間的聯系.
在ContainerViewController裡添加這麼一個屬性.
private var detailViewController: DetailViewController?
然後override我們的ContainerViewController裡的prepareForSegue(_:sender:)方法.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "DetailViewSegue" { let navigationController = segue.destinationViewController as! UINavigationController detailViewController = navigationController.topViewController as? DetailViewController } }
別忘了設置我們的segue.identifier.如圖所示.
然後再添加一個menuItem的屬性到ContainerViewController裡,並且監聽如果menuItem被設置,那麼讓detailViewController的menuItem相應的也改變.
var menuItem: NSDictionary? { didSet { if let detailViewController = detailViewController { detailViewController.menuItem = menuItem } } }
然後,到我們的MenuViewController裡,先刪除prepareForSegue這個方法,因為這個方法是以前MenuViewController和DetailViewController有直接關聯的時候才有用的,現在這個方法顯然已經沒有意義了.
我們要做的就是在MenuViewController裡的tableview 的delegate裡添加以下的內容.
// MARK: UITableViewDelegate override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { tableView.deselectRowAtIndexPath(indexPath, animated: true) let menuItem = menuItems[indexPath.row] as! NSDictionary (navigationController!.parentViewController as! ContainerViewController).menuItem = menuItem }
然後再在ViewDidLoad()方法裡加入以下內容,確保第一次進入頁面的時候默認選擇的是第一個Cell.
(navigationController!.parentViewController as! ContainerViewController).menuItem = (menuItems[0] as! NSDictionary)
運行一下.效果如下.
顯示和隱藏我們的Menu
現在我們點擊cell雖然DetailViewController的內容可以正確顯示,但是菜單並不能自動隱藏.所以我們首先要實現的是點擊菜單之後菜單自動隱藏.
要實現這個效果,首先要把我們的ContainerViewController裡的scrollView和MenuContainerView拖線拖到我們的ContainerViewController裡.
如圖.
然後給ContainerViewController.swift添加一個新的方法.
hideOrShowMenu(_:animated:)
// MARK: ContainerViewController func hideOrShowMenu(show: Bool, animated: Bool) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) scrollView.setContentOffset(show ? CGPointZero : CGPoint(x: menuOffset, y: 0), animated: animated) }
然後在MenuItem的didSet裡加入這個方法,意思就是每次設置menuItem的時候都會自動調用這個方法.
override func viewDidLoad() { super.viewDidLoad() hideOrShowMenu(false, animated: false) }
運行一下.
原文中提到了這時候菜單還是存在回彈和收不回去的問題,實際上在我做的時候並沒有出現這種情況.所以如果你們做的時候如果出現了回彈.那麼需要在ContainerViewController裡實現UIScrollView的這個Delegate.
// MARK: - UIScrollViewDelegate func scrollViewDidScroll(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)) }
然後運行,這時候應該沒問題了.
添加我們的漢堡按鈕
新建一個類繼承自UIView,起名叫做HamburgerView.swift.
然後修改內容如下.
class HamburgerView: UIView { let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”)) required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configure() } required override init(frame: CGRect) { super.init(frame: frame) configure() } // MARK: Private private func configure() { imageView.contentMode = UIViewContentMode.Center addSubview(imageView) } }
然後在我們的DetailViewController裡,把他加進去.先添加一個屬性
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!)
這個手勢的事件hamburgerViewTapped()會調用 ContainerViewController’s hideOrShowMenu(_:animated:),但是現在缺少一個布爾值來表示菜單是否處於打開狀態.所以我們為ContainerViewController添加一個布爾值用來記錄菜單的狀態.
var showingMenu = false
然後override viewDidLayoutSubviews()方法.加入如下代碼.
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() hideOrShowMenu(showingMenu, animated: false) }
這會在ContainerViewController的布局每次發生變化的時候調用hideorShow方法.
然後打開DetailViewController,添加我們的點擊事件.
func hamburgerViewTapped() { let navigationController = parentViewController as! UINavigationController let containerViewController = navigationController.parentViewController as! ContainerViewController containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true) }
現在點擊漢堡按鈕,已經能夠打開菜單了,但是再次點擊應該是關閉菜單,然後並沒有效果,原因很簡單,你沒有跟新showingMenu的值,所以在我們的hideOrShowMenu方法裡加入showingMenu = show.
再試一下.
ok了.
然而,問題依然沒有結束.
當你滑動打開菜單的時候,需要點擊漢堡菜單兩次才能關閉菜單.這是因為你滑動打開菜單的時候並沒有更新showingMenu的值.所以,需要在UIScrollviewDelegate裡更新我們的showingMenu.
func scrollViewDidEndDecelerating(scrollView: UIScrollView) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset) println(“didEndDecelerating showingMenu \(showingMenu)”) }
運行一下,注意一下console,當你快速滑動的時候是沒問題的,但是緩慢滑動的時候這個方法似乎不響應.所以這個方法並不靠譜.
我們把代碼移到另一個代理方法scrollViewDidScroll(_:)裡.
再次運行.
應該沒問題了.
給我們的菜單添加透視效果
實際上完整的效果華麗就華麗在菜單出現的方式並不是水平的,而是以3D旋轉的效果出現的.要實現這個效果我們必須計算菜單顯示的比例和菜單旋轉角度之間的關系.如下所示.
func transformForFraction(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.5 let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0) let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0) return CATransform3DConcat(rotateTransform, translateTransform) }
上面的方法就是計算菜單顯示的部分和旋轉角度的關系.
fraction當menu完全隱藏的時候是0,完全顯示的時候是1.
CATransform3DIdentity代表原始的Transform.
CATransform3DIdentity’s m34這個值代表view的perspective.(設置了他旋轉的時候才會有3D效果)
利用CATransform3DRotate來實現菜單的旋轉效果.並且是繞Y軸旋轉.-90度的時候代表與平面向內垂直(所以你看不到).0度的時候水品展開.
translateTransform負責menu在旋轉的時候同時位移到正確的位置.
CATransform3DConcat負責把位置的transform和旋轉的transform結合起來.
現在在我們的scrollViewDidScroll這個代理方法裡加入以下代碼.
let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds) let offset = scrollView.contentOffset.x * multiplier let fraction = 1.0 - offset menuContainerView.layer.transform = transformForFraction(fraction) menuContainerView.alpha = fraction
運行一下.
效果似乎不太對.那是因為我們並沒有設置menuContainerView的anchorpoint,現在的anchorPoint還是在view的中心點我們實際上的anchorpoint應該是在view中心最右的位置.所以在ContainerViewController的viewDidLayoutSubViews()裡修改anchorPoint.
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
運行.
效果不錯.
讓漢堡按鈕動起來.
我們只剩最後一個效果了,就是菜單出現的過程中,漢堡按鈕也要轉相應的角度.
在HamburgerView中添加下面的方法.
func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle)) }
然後在ContainerViewController裡的scrollViewDidScroll()裡添加以下代碼.
if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) } }
運行一下.
Perfect!
從這裡獲取最終的程序.
下載
如果你對perspective有疑問.那麼請在這裡浏覽相關信息.
Perspective)
有任何疑問可以留言.