當一個使用者按下選單按鈕,主畫面下滑揭示了選單。如下圖是在Medium App中使用到下滑選單的畫面。
倘若你前面的章節有跟著一起進行,你應該對客製視圖控制器轉換有了基本的了解。本章,你將運用你所學到的來建造一個生動的下滑選單。
依照慣例,我想你不需要從頭建立專案,建議可以使用我們準備好的範例模板來開始,它包含了Storyboard 以及視圖控制器類別。你將會發現兩個視圖控制器。一個是主畫面(嵌入至導覽控制器中),而另一個導覽選單。倘若你執行專案,這個App應該會出現一個主畫面加上一些虛構的資料。 繼續往下進行之前,先花個幾分鐘瀏覽一下這個程式模板以熟悉一下專案內容。
以Modal 方式呈現選單
好的,我們開始吧。首先打開 Main.storyboard檔。你應該會找到兩個還沒有連上任何Segue的表格視圖控制器,為了在使用者按下選單按鈕時能帶出選單,按住control鍵不放,從選單按鈕拖曳至選單表格視圖控制器(menu table view controller)。將按鈕釋放,然後選取「present modally」作為Segue的動作。
倘若你執行專案,這個選單將會以modal視圖的方式來呈現。為了關閉(unwind)選單,我們會加入unwind Segue。
打開NewsTableViewController.swift 檔並插入一個unwind動作方法:
@IBAction func unwindToHome(segue: UIStoryboardSegue) {
let sourceController = segue.sourceViewController as MenuTableViewController
self.title = sourceController.currentItem
}
現在回到Storyboard,按住control 鍵不放,從Menu table view controller的prototype cell 拖曳至exit 圖示。提示出現之後,在selection segue選擇unwindToHome:
選項。
現在當使用者按下任何選單項目,選單控制器便會解除,進而揭示主畫面。透過 unwindToHome:
動作方法,這個主視圖控制器(也就是NewsTableViewController
)依照使用者在選單項目上的選取來變更其標題。為了簡單起見,我們只是變更導覽欄(navigation bar)的標題,而不會改變主畫面的內容。
另外,主視圖控制器會將所選取的項目以白色來標示,想讓App能如預期般順利運作前,還有幾個方法要先實作。
在MenuTableViewController
類別插入以下的方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let menuTableViewController = segue.sourceViewController as MenuTableViewController
if let selectedRow = menuTableViewController.tableView.indexPathForSelectedRow()?.row {
currentItem = menuItems[selectedRow]
}
}
這裏我們只是設定 currentItem
為選單選取的項目。
在NewsTableViewController.swift 檔,插入下面的方法來傳遞目前的標題至選單控制器:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let menuTableViewController = segue.destinationViewController as MenuTableViewController
menuTableViewController.currentItem = self.title!
}
現在編譯與執行專案。按下選單項目,App便會以modal方式來呈現選單,當你選取選單項目,選單會關閉,導覽欄標題也會跟著變更。
建立生動的下滑選單
現在選單是使用標準動畫來呈現,我們來建立自訂轉換。如同我前面章節所述,客製視圖控制器動畫的核心,是動畫物件遵循了UIViewControllerAnimatedTransitioning
與UIViewControllerTransitioningDelegate
協定。我們準備實作這個類別。但是我們先來看下滑選單的運作方式。當使用者按下選單,主視圖開始下滑直到它到達預定的位置,也就是離畫面底部150點處。以下的圖片說明可以讓你對滑動選單更有概念。
建立下滑選單動畫
想要建立下滑特效,我們會建立一個稱作MenuTransitionManager
的下滑動畫。在專案導覽器,按右鍵來建立一個新檔案。將類別取作MenuTransitionManager
,並將其設為NSObject
的子類別。
更新類別如下:
class MenuTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate { var duration = 0.5 var isPresenting = false var snapshot:UIView? func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return duration } func animateTransition(transitionContext: UIViewControllerContextTransitioning) { // 取得我們的 fromView、toView 以及 container view的參照 let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)! let toView = transitionContext.viewForKey(UITransitionContextToViewKey)! // 設定滑動的變換(transform) let container = transitionContext.containerView() let moveDown = CGAffineTransformMakeTranslation(0, container.frame.height - 150) let moveUp = CGAffineTransformMakeTranslation(0, -50) // 將兩個視圖加進容器視圖 if isPresenting { toView.transform = moveUp snapshot = fromView.snapshotViewAfterScreenUpdates(true) container.addSubview(toView) container.addSubview(snapshot!) } // 執行動畫 UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.3, options: nil, animations: { if self.isPresenting { self.snapshot?.transform = moveDown toView.transform = CGAffineTransformIdentity } else { self.snapshot?.transform = CGAffineTransformIdentity fromView.transform = moveUp } }, completion: { finished in transitionContext.completeTransition(true) if !self.isPresenting { self.snapshot?.removeFromSuperview() } }) } func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { isPresenting = false return self } func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { isPresenting = true return self } }
這個類別實作UIViewControllerAnimatedTransitioning
與 UIViewControllerTransitioningDelegate
協定。我不準備解釋這個方法的細節,因為前面章節已經解釋過了。我們將重點放在動畫block(也就是animateTransition 方法)。
參考前面圖片說明,在轉換期間主視圖是 fromView
,此時選單視圖是toView
。
要建立動畫,我們設置兩個變換。第一個變換(也就是 moveDown)是用來下移主視圖。第二個變換(也就是 moveUp)是設定將選單視圖上移一點,所以當它回復到原來位置時,它會有下滑效果。稍後你執行專案,你將會瞭解我的意思。
從iOS 7 開始,你可以使用UIView-Snapshotting API 來快速且輕易的建立一個輕量級(light-weight)的視圖快照(snapshot)。
snapshot = fromView.snapshotViewAfterScreenUpdates(true)
透過snapshotViewAfterScreenUpdates
方法的呼叫,你有一個主視圖的快照。有了快照,我們可以將其加入容器視圖來執行動畫。注意這個快照是加到選單視圖的上方。
選單呈現的實際動畫,其實作是非常簡單的。我們只是對主視圖的快照實施moveDown 變換,並將選單視圖回復其預設位置。
self.snapshot?.transform = moveDown
toView.transform = CGAffineTransformIdentity
當選單解除時,逆向效果便開始發生。主視圖的快照向上滑動並回到其預設位置。另外快照會從它的父視圖被移除,也因此我們可以將實際主視圖帶回。
現在打開NewsTableViewController.swift 並宣告一個變數給MenuTransitionManager
物件:
var menuTransitionManager = MenuTransitionManager()
在 prepareForSegue
方法,加上一行變數來串接動畫:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let menuTableViewController = segue.destinationViewController as MenuTableViewController
menuTableViewController.currentItem = self.title!
menuTableViewController.transitioningDelegate = self.menuTransitionManager
}
就這樣!你現在可以編譯與執行專案。按下選單按鈕,你將會得到一個下滑選單。
偵測按下手勢
現在唯一關掉選單的方式就是選取選單項目。從使用者的觀點,按下快照應該也要能關掉選單。不過,主視圖的快照沒有反應。
這個快照實際上是一個UIView
物件。因此我們可以建立一個UITapGestureRecognizer
物件並將其加至快照。
實體化一個UITapGestureRecognizer
物件,我們需要傳給它一個目標物件(target object),也就是接收由接收者所發送的動作訊息,且會呼叫動作方法。
很明顯地,你可以將特定物件寫進去,作為目標物件來解除視圖。但是為了讓設計有所彈性,我們會定義一個協定讓代理物件來執行它。
在MenuTransitionManager.swift,定義下面的協定:
@objc protocol MenuTransitionManagerDelegate {
func dismiss()
}
這裏我們以必要實作做的方法來定義一個MenuTransitionManagerDelegate
協定。這個代理應該要實作「dismiss」方法,並提供解除或關閉視圖的實際邏輯。
在MenuTransitionManager
類別,宣告代理變數:
var delegate:MenuTransitionManagerDelegate?
稍後要負責處理按下手勢的物件,應該設為代理物件。
最後,我們需要建立一個UITapGestureRecognizer
物件並將其加入快照中。一個比較好的方式是在snapshot
變數中定義一個 didSet
方法。變更snapshot
宣告如下:
var snapshot:UIView? {
didSet {
if let _delegate = delegate {
let tapGestureRecognizer = UITapGestureRecognizer(target: _delegate, action: "dismiss")
snapshot?.addGestureRecognizer(tapGestureRecognizer)
}
}
}
屬性觀察者(Property observer)是Swift中一個強大的功能。每次屬性的值設定後會呼叫觀察者(willSet/didSet)。這提供了在指派之前或之後,立即執行某個動作的便利方式。willSet
方法在值被儲存前會被呼叫,而 didSet
方法在指派後會立即呼叫。
在以上的程式中,我們使用屬性觀察者來建立一個手勢辨識器(gesture recognizer)並設定給快照。所以在每次指派一個物件給snapshot
變數時,它會立即設置按下手勢辨識器(tap gesture recognizer)。
我們幾乎已經完成了。現在回到NewsTableViewController.swift,也就是要實作MenuTransitionManagerDelegate
協定的類別。
首先,變更類別宣告如下:
class NewsTableViewController: UITableViewController, MenuTransitionManagerDelegate
接下來,實作協定所需的方法:
func dismiss() {
dismissViewControllerAnimated(true, completion: nil)
}
這裏我們只是透過dismissViewControllerAnimated
方法的呼叫來解除視圖控制器。
最後,在NewsTableViewController
類別的prepareForSegue
方法中插入下面一行程式,來設定它自己為代理物件:
self.menuTransitionManager.delegate = self
很棒!你已經準備好再次測試App了,按下Run按鈕來測試。你應該能夠在按下主視圖的快照後解除選單。
如果將自訂的視圖控制器轉換運用得宜,它能夠很棒的改善使用者體驗,讓你的App與眾不同。下滑選單只是一個例子。試著在你的下一個建立你自己的動畫。
為了進一步讓您參考,你可以在這裡下載完整的 Xcode 專案。