原攻略來自于:How To Create a Cool 3D Sidebar Animation Like in Taasky
本篇文章為我自己的總結蛤高、翻譯版,你可以點擊上面的鏈接找到原文(如果你不是簡書看到的,可以點擊這里查看我的原文)
在今天的文章中筐骇,由于我的英文能力有限讶舰,就不會對原文進行逐句翻譯了何缓,我會以自己的理解來講解這篇文章的精髓随橘,教你一步步來實現最終的效果
另外由于我個人的能力有限,可能有些地方出現遺漏或者理解出錯聚假,歡迎指正下块蚌,共同學習。
最終效果:
![最終效果](https://koenig-media.raywenderlich.com/uploads/2015/03/runfinal.gif)
由于之前作者是用swift老版本的初始工程來講解的膘格,你可以來這里下載swift版本轉換好的源文件開始今天的學習
(ps:接下來的工程我都用swift3.0實現了一遍峭范,不過仍可能出現粘貼的是老代碼未及時更新的問題,不過swift版本的問題也不難瘪贱。)
下載好后纱控,Taasky_origin就是我們要開始的初始文件了。
點擊Run運行菜秦,你可以看到如下效果:
![首頁甜害、詳情頁](https://koenig-media.raywenderlich.com/uploads/2015/03/starterMasterDetail.png)
看起來像是一個工作之余和朋友喝喝咖啡之類的App
我們現在要做如下的事情
- 首先你要創(chuàng)建一個ScrollView來包含左側的側邊欄按鈕部分和右邊的詳情部分
- 然后添加一個按鈕來控制側邊欄的顯示和隱藏
- 接著實現一個3D 效果來完成平滑的折疊效果
- 最后當你觸摸的時候球昨,我們順帶添加一個菜單旋轉的動畫尔店,和我們顯示、隱藏側邊欄同步起來
我們來分析一下
![具體來看](https://koenig-media.raywenderlich.com/uploads/2015/03/diagramMenuDetail1-225x320.png)
- 當菜單顯示的時候我們實際看到的是紫色部分的區(qū)域
- 當菜單隱藏的時候我們看到的是綠色的區(qū)域
接下來我們會用到ContainerView 主慰,并通過自動布局添加約束的方式來完成我們最終的布局嚣州,最終我們要讓storyboard出現下面的畫面,提前來預覽下吧:
![最終我們將會看到的storyboard](https://koenig-media.raywenderlich.com/uploads/2015/03/sbContainers.png)
不要著急共螺,我們接下來會一步步實現的该肴。
給UIScrollView添加約束
接下來我們需要做的是:創(chuàng)建一個新的控制器來協調菜單控制器和詳情控制器,在這個新的控制器中我們添加一個UIScrollView藐不,然后給ScrollView添加 兩個container views 匀哄,分別嵌入菜單控制器和詳情控制器秦效。
創(chuàng)建一個新的ContainerViewController繼承自UIViewController,語言選擇Swift
-
在Main.storyboard拖拽一個新的UIViewController拱雏,在Inspector中修改下之前Class\Custom Class 為 ContainerViewController
把背景改為黑色:
-
到了添加ScrollView的時候了棉安,我們拖拽一個ScrollView底扳,并把大小拉至填充整個屏幕铸抑,在右側屬性檢測器中去掉顯示指示條,并且把Delays Content Touches去掉勾選衷模,這樣當你選中的時候就不會有那么一點延遲了
設置ScrollView 的delegate為當前控制器
給ScrollView添加四個約束鹊汛,記得去掉勾選Constrain to margins(不要在意下圖中的寬高值的問題):
添加的約束是:
Trailing Space to: Superview
Leading Space to: Superview
Top Space to: Superview
-
Bottom Space to: Bottom Layout Guide
添加完后的樣子 該給ScrollView添加內容了,拖拽一個UIView給ScrollView阱冶,讓它的大小和ScrollView的大小一樣刁憋,然后再通過右側屬性檢測器給這個UIView的寬度增加80,舉例:我在7Plus的尺寸下編輯木蹬,ScrollView由于完全占滿屏幕所以寬度是414至耻,那么我們剛剛添加的UIView的尺寸就是494了(這個時候我們還沒有添加布局哦)
-
修改添加的UIView的Document\Label為Content View(主要是為了方便追蹤各個view)
Open the Identity Inspector for the content view you just added and set
給剛才的UIView添加約束
-
修改一下Trailing Space的 Constant 為0(??,好吧在上面一步你完全可以直接設置好的镊叁,多做一步只是為了讓新手熟悉一下...)
設置好后尘颓,你會發(fā)現有布局的警告,這是因為對于ScrollView的布局僅僅設置上下左右是不夠的晦譬,還要對Content View的寬高做設置疤苹,這樣才能決定ScrollView的 content size.
-
接下來我們來設置寬和高,選中Content View敛腌,按住control鍵拖拽至View卧土,如下:
在右側屬性檢測器中修改寬度的常量為80
設置常量為80的意思是我們的Content View會比整個View(也相當于屏幕的寬度)的寬度寬80
好了,現在再看看像樊,發(fā)現警告不見了吧尤莺,哈哈,太棒了生棍!
添加菜單和詳情的Container Views
拖拽一個UIContainer View到Content View上颤霎,在屬性檢測器中設置寬度為80,并且給 Document\Label設置為Menu Container View足绅,如下:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbContainerMenuUtil.png)
不要在意圖中的600捷绑,因為Xcode版本的問題,現在的Xcode8已經不是默認600X600了氢妈,我們要設置的是高度和Content View一樣粹污,寬度為80,左上角對齊父視圖就對了
添加詳情Container View:同樣再拖拽一個UIContainer View放在菜單Container View右邊首量,給它的Document\Label 設置為Detail Container View
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbContainerDetailUtil.png)
這個詳情Container View的寬度和父視圖的寬度是一樣的壮吩,現在你應該得到如下的視圖:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbContainerViews1.png)
添加Container View 會自帶一個控制器进苍,把它們刪掉:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbDeleteContainerVCs.png)
現在來設置布局吧:
對于Menu Container View:相對于它的父視圖以及詳情 Container View共5個約束:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbAutoMenu.png)
對于 Detail Container View:添加了3個約束:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbAutoDetail.png)
通過移動箭頭改一下 Initial view controller
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbMoveStartArrow.png)
把菜單控制器和詳情控制器嵌套進來:選中Menu Container View拖拽到Navigation Controller,并選擇embed
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbEmbedMenu.png)
設置好嵌套之后鸭叙,你的storyboard會變成如下:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbEmbedResult.png)
嵌套進來的控制器統(tǒng)統(tǒng)都收縮到80的寬度了觉啊。
調整一下menu view controller上UIImageView的寬度
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbSizeInspImage.png)
刪掉 menu和 detail 之間的 segue,并給detail view controller添加一個Navigation Controller 通過上面菜單欄的按鈕:Editor\Embed In\Navigation Controller
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbEmbedDetailNavController.png)
給這個新的navigation controller 設置如下屬性:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbNavBarAttr.png)
在屬性檢測器中設置View Controller\Layout\Adjust Scroll View Insets選中(這個會避免內容被bar覆蓋):
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbNavControllerLayout.png)
我們要讓Detail View Controller嵌套在Detail Container View中沈贝,
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbEmbedDetail-321x320.png)
現在你就可以運行一下了杠人,運行結果應該是這個樣子:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runScrollView0a.gif)
現在我們可以隨意滑動ScrollView了, 接下來我們要修改一下只讓它顯示整個側邊菜單和隱藏側邊菜單宋下, 并且讓它不能滑出邊界
- 在ScrollView的屬性檢測器中選擇Scrolling\Paging Enabled
- 不要選擇Bounce\Bounces
再次build&run 你會發(fā)現:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runScrollview0b.gif)
但是還有一點點問題嗡善,當你試圖隱藏菜單框的時候,它有時又重新彈回并顯示出來了学歧,這是一個 paging的問題罩引,詳細的你可以參考這里: this problem discussion on StackOverflow.
先留著這個問題,我們先來解決點擊左側按鈕沒有相應的事件變化的問題(和我們最上面演示的圖作比較)
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runScrollview1.gif)
這或許會使你感到驚訝枝笨,因為我們并沒有改變任何相關的代碼袁铐,接著往下看:
先修改一下細節(jié)的問題:
在 MenuViewController.swift的 viewDidLoad() 加入以下代碼:
override func viewDidLoad() {
super.viewDidLoad()
// Remove the drop shadow from the navigation bar
navigationController!.navigationBar.clipsToBounds = true
}
這能消除掉navigation bar下極小的細縫,雖然是個很小的細節(jié)横浑,但是也能對整個APP填色不少剔桨。
當我們點擊MenuViewController中的cell的時候 應該設置DetailViewController 的 menuItem 屬性,但是現在DetailViewController已經不直接和MenuViewController連接了伪嫁,所以沒什么反應
ContainerViewController將會扮演MenuViewController和DetailViewController之間的協調者的角色
給ContainerViewController.swift添加一個屬性:
private var detailViewController: DetailViewController?
在ContainerViewController.swift中實現 prepareForSegue(_:sender:) 方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "DetailViewSegue" {
let navigationController = segue.destination as! UINavigationController
detailViewController = navigationController.topViewController as? DetailViewController
}
}
在將DetailViewController嵌套進container View中的時候會生成一條Storyboard Embed Segue的線领炫,你選中這條線,并設置它的Identifier為DetailViewSegue:
![](https://koenig-media.raywenderlich.com/uploads/2015/06/sbsegueID.png)
在ContainerViewController中聲明一個menuItem屬性张咳,并設置屬性觀測器:
var menuItem: NSDictionary? {
didSet {
if let detailViewController = detailViewController {
detailViewController.menuItem = menuItem
}
}
}
由于MenuViewController 和DetailViewController之間已經沒有segue連接了帝洪,但是當選中MenuViewController中的cell的時候仍然需要作出相應,我們把事件的相應從prepareForSegue(:sender:)移動到tableView(:didDeselectRowAtIndexPath:)中脚猾。
刪掉prepareForSegue(_:sender:)中的代碼葱峡,改成如下:
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let menuItem = menuItems[indexPath.row] as! NSDictionary
(navigationController!.parent as! ContainerViewController).menuItem = menuItem
}
這樣我們就在選中menu中的cell的時候設置ContainerViewController的menuItem屬性了,并且觸發(fā)了屬性觀測器從而設置了DetailViewController的menuItem屬性
我們在MenuViewController.swift中的viewDidLoad()添加:
(navigationController!.parentViewController as! ContainerViewController).menuItem =
(menuItems[0] as! NSDictionary)
這是為了app第一次啟動的時候給DetailViewController設置默認的圖片
現在運行app龙助,會發(fā)現如下效果了:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runScrollView2.gif)
現在我們要顯示和隱藏左側菜單
當我們選中菜單的時候砰奕,我們要隱藏掉菜單
為了實現這個目的,我們要設置Scroll View 的content
在ContainerViewController.swift 中連接 Scroll View并命名為scrollView
具體操作如下:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/sbConnectScrollView.png)
現在在ContainerViewController.swift 添加 hideOrShowMenu(_:animated:)方法
// MARK: ContainerViewController
func hideOrShowMenu(show: Bool, animated: Bool) {
let menuOffset = menuContainerView.bounds.width
scrollView.setContentOffset(show ? CGPoint.zero : CGPoint(x: menuOffset, y: 0), animated: animated)
}
menuOffset 的值是 80 提鸟,當true的時候军援,那么origin就是(0,0),這個時候菜單是可見的称勋,同理胸哥,當origin時(80,0)的時候菜單是隱藏的
在ContainerViewController的menuItem屬性觀測器中添加
var menuItem: NSDictionary? {
didSet {
hideOrShowMenu(false, animated: true)
// ...
運行app赡鲜,將會出現如下效果:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runScrollView3.gif)
好了這個時候我們來解決paging 的問題吧(滑動來隱藏側邊欄的時候空厌,側邊欄會彈出來的BUG)
我們將通過遵守UIScrollViewDelegate協議來解決這個問題
給ContainerViewController添加協議:
class ContainerViewController: UIViewController, UIScrollViewDelegate {
添加協議方法并實現:
// 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.isPagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - scrollView.frame.width)
}
對于這個問題的詳細探究就不在本文的范圍之內了庐船,有興趣的同學可以進入這個鏈接看看:這里
Bulid&Run,你會發(fā)現解決了這個問題:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runScrollView4.gif)
添加左上角的按鈕
通過點擊按鈕來隱藏和展示左側欄,創(chuàng)建一個HamburgerView.swift繼承于UIView
其內部實現:
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.swift添加一個屬性:
var hamburgerView: HamburgerView?
在 viewDidLoad()中創(chuàng)建hamburgerView并賦在navigation bar上
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的hideOrShowMenu(_:animated:)方法嘲更,但是我們應該傳入什么值呢筐钟?
我們給ContainerViewController添加一個bool類型的屬性來記錄左側菜單是否顯示
在ContainerViewController.swift下面添加屬性:
var showingMenu = false
我們重寫viewDidLayoutSubviews() 來控制展示或者隱藏菜單欄,這樣的好處是旋轉的時候也能及時響應:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
hideOrShowMenu(show: showingMenu, animated: false)
}
那么我們就不需要ContainerViewController.swift中的viewDidLoad()了赋朦,刪了它們吧
別忘了我們還要在DetailViewController.swift中添加響應事件:
func hamburgerViewTapped() {
let navigationController = parent as! UINavigationController
let containerViewController = navigationController.parent as! ContainerViewController
containerViewController.hideOrShowMenu(show: !containerViewController.showingMenu, animated: true)
}
每當我們點擊的時候需要通過!containerViewController.showingMenu來控制是否顯示篓冲,就像button的選中非選中那樣。
我們要在hideOrShowMenu方法中及時修改一下我們的showingMenu的狀態(tài)北发,在hideOrShowMenu方法下面添加如下:
showingMenu = show
B&R(Build & Run)你會看到如下的效果:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runButton1a.gif)
還有一個問題就是當你滑動展示菜單欄或者隱藏菜單欄的時候纹因,再去點擊左上角的button來響應事件需要點擊兩次喷屋,這是為什么呢琳拨?
這是因為我們滑動ScrollView的時候 并沒有更新showingMenu
為了修正這個問題,你需要實現UIScrollViewDelegate另一個方法
在ContainerViewController中添加:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let menuOffset = CGRectGetWidth(menuContainerView.bounds)
showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset)
println(“didEndDecelerating showingMenu \(showingMenu)”)
}
當ScrollView的content offset等于菜單欄寬度(80)的時候屯曹,也就是菜單欄是隱藏的狱庇,設置showingMenu為false,反之同理
B&R 讓我們來看看當停下來的時候是否會如預期那樣恶耽?看起來和期望好像有一點點的差池密任,這點差池有點依賴于滑動的速度,當我在模擬器上測試的時候偷俭,只有滾動很慢的情況下達到預期浪讳,但是在真實設備上只有滾動很快才會達到預期。
好吧涌萤,那就把上面的代碼全部都移動到scrollViewDidScroll(_:)中淹遵,這個方法會不斷的調用,相對來說更可靠點
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runButton2.gif)
添加3D效果
首先要添加透視效果:
在ContainerViewController.swift中添加如下代碼:
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 = menuContainerView.bounds.width * 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)
}
分析一下transformForFraction(_:):的作用:
- 當 fraction為0的是菜單欄完全隱藏负溪,當fraction為1的時候菜單欄完全顯示
- CATransform3DIdentity 是4x4的單位juzhen
- CATransform3DIdentity的m34屬性控制著透視的量
- CATransform3DRotate 用弧度制 來控制繞y軸的旋轉量透揣,-90表明垂直于當前的x-y平面,0表明平行于x-y平面
- rotateTransform是單位矩陣經傳入m34 值按照一定弧度選擇變換之后的矩陣
- translateTransform 是將菜單欄向右移動其一半寬度距離變換而來的矩陣
- CATransform3DConcat 將上面的兩個矩陣進行了連鎖變化
注意: m34 通常是1除以一個值來表示川抡,這個值表達的含義是你在z軸上觀察x-y平面的位置(單位是像素)辐真,負數表明觀察者是在屏幕前徐裸,而正數表示觀察者在屏幕后面粗合。
在觀察者和觀察的對象之間畫線形成3D透視效果。觀察者移動的越遠岭接,那么透視效果越不明顯密幔。你可以試試改變值1000到500或者2000來看看菜單欄會發(fā)生什么樣的變化.
在scrollViewDidScroll(_:):下添加如下代碼:
let multiplier = 1.0 / menuContainerView.bounds.width
let offset = scrollView.contentOffset.x * multiplier
let fraction = 1.0 - offset
menuContainerView.layer.transform = transformForFraction(fraction: fraction)
menuContainerView.alpha = fraction
值是從0到1的楔脯,0表示完全顯示,1表示完全隱藏菜單欄
這樣的話fraction 完全依賴于已經顯示的菜單欄的寬度來改變其值(0~1),同時我們還通過fraction來調整菜單欄的alpha 來改變它的明暗情況
B&R 我們可以看到我們的3D效果了
![](https://koenig-media.raywenderlich.com/uploads/2015/04/runwonkyhinge-180x320.png)
很顯然還有一點錯誤老玛,我們的連接點好像出問題了淤年,這是因為view默認狀態(tài)的anchorPoint是view的中心
我們來修改anchorPoint使其在右側邊緣中心:
在ContainerViewController.swift的viewDidLayoutSubviews()中添加如下code:
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
B&R 你會看到:
![](https://koenig-media.raywenderlich.com/uploads/2015/03/runPenultimate.gif)
還有一件事:
我們來給左上角的按鈕(以下稱MenuBtn)添加動畫吧
在HamburgerView.swift中添加:
func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle)) }
在ContainerViewController.swift中的scrollViewDidScroll(_:)中添加如下代碼:
if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) } }
B&R :
![最終效果](https://koenig-media.raywenderlich.com/uploads/2015/03/runfinal.gif)
Cool钧敞!