序言
簡單說,這段時間開發的時候有個業務需要用側滑菜單來實現。博主當時的第一反應是上網找輪子直接使用,然而,事情總是出乎意料的。使用網上的開源輪子之后,我在點擊tabBarController的切換控制器時,卻意外的crash了。
什么鬼!待博主看完源碼,發現找到的輪子幾乎都是將我們當前要顯示的主界面view以及側邊欄的view加到輪子的view上,然后設置輪子為rootViewController進行管理。因此,當博主將tabBarController作為側滑功能的mainController的時候,雖然自己的view能夠正常顯示,但是tabBar點擊的切換功能就沒了,博主就這樣作死了自己。
然而博主真漢子,不會就這樣輕易的狗帶,于是決定自己實現側滑的功能,最后的效果如下(看官們有能用于tabBarController的側滑輪子也可以推薦推薦):
解決思路
我列出了側滑的兩種效果:
上面已經提出了一些輪子的錯誤:tabBarController不為rootViewController的情況下,切換控制器的功能無法實現。因此,解決思路包括了將側邊欄視圖加在tabBarController或者加在keyWindow上,從功能實現上而言,直接加在keyWindow上是最省事的。
在keyWindow上實現
將側滑邊欄視圖加在keyWindow上的同時,我還動態將視圖所在的控制器和keyWindow綁定在一起,這樣可以避免邊欄視圖的控制器被釋放,從而導致的交互事件無法回調。注意使用keyWindow的前提是我們自定義了AppDelegate的window入口,當然在這里并不是很推薦這種方法
let keyWindow: UIWindow = UIApplication.sharedApplication().keyWindow;
keyWindow.addSubview((menuController.view)!)
objc_setAssociatedObject(keyWindow, kMenuControllerKey, slideMenuController, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
在我們產生點擊事件進行側滑的時候,應當移動不在當前可視范圍內的邊欄視圖,移動視圖的代碼如下:其中maxOpenRatio表示移動的屏幕寬度最大比例。
var frame = self.slideController?.view.frame
frame?.origin.x = self.slideDirection == .Left ? kWidth*(1-maxOpenRatio) : -kWidth*(1-maxOpenRatio)
UIView.animateWithDuration(0.2) { () -> Void in
self.slideController?.view.frame = frame!
}
如果我們要做的是第一種效果,那么在上面這段代碼中獲取主視圖的frame然后修改x坐標實現動畫效果進行位移。在我們移動之后,我們應該在主視圖上面添加一個單擊手勢以便我們點擊后還原側滑效果。當然,這個手勢應該在還原動畫完成的時候移除掉。因此側滑的動畫代碼如下:
private func open() {
isOpen = true
self.mainController?.view.addGestureRecognizer(self.tap!)
var frame = self.slideController?.view.frame
frame?.origin.x = self.slideDirection == .Left ? kWidth*(1-maxOpenRatio) : -kWidth*(1-maxOpenRatio)
UIView.animateWithDuration(0.2) { () -> Void in
self.slideController?.view.frame = frame!
}
}
private func close() {
isOpen = false
var frame = self.slideController?.view.frame
frame?.origin.x = self.slideDirection == UIScreen.mainScreen.bounds.size.width
self.slideController?.view.endEditing(true)
UIView.animateWithDuration(0.2, animations: { () -> Void in
self.slideController?.view.frame = frame!
}, completion: { (finished: Bool) -> Void in
self.mainController.view.removeGestureRecognizer(self.tap)
})
}
使用keyWindow的情況下,已經完成了我需要的效果。但是,這種方式博主并不推薦。在window上面添加視圖可能會引發不必要的麻煩甚至一些隱晦的crash,因此我們應該使用tabBarController的view來完成側滑效果
在tabBarController上實現
第二種實現側滑的方式是直接將側滑的視圖加在當前的主視圖上面,并且保證側滑視圖顯示在屏幕外
func init(mainController: UIViewController!, menuController: UIViewController!) {
super.init()
self.mainController = mainController
self.menuController = menuController
let menuFrame: CGRect = CGRectOffset(UIScreen.mainScreen.bounds, kWidth, 0)
menuController.view?.frame = menuFrame
mainController.view.addSubview((menuController.view)!)
}
此時,使用上面兩種動畫效果時。由于側滑視圖已經加在主視圖上面了,如果是要保持主視圖和菜單欄同時移動的效果,那么我們只需要移動主視圖。或者只移動側滑視圖來實現菜單左移的效果
func open() {
var mainFrame = self.menuController?.view.frame
var menuFrame = self.menuController?.view.frame
if slideType == .BothMove {
mainFrame.origin.x = -kMaxOpenRatio * kWidth
} else if slideType == .MenuMove {
menuFrame.origin.x = (1 - kMaxOpenRatio) * kWidth
}
animateWithDuration(0.2, animation: { () -> Void in
self.mainController?.view.frame = mainFrame
self.menuController?.view.frame = menuFrame
}, completion: { (finished: Bool) -> Void in
self.mainController?.view.addGestureRecognizer(self.tap!)
})
}
在移動動畫發生完成之后,我們給主視圖加上一個點擊手勢方便再次點擊的時候還原位移。在還原的時候我們也應該從主視圖上面移除這個點擊事件——除非你想發生一些有趣的bug。寫完這段移動代碼,效果如下:
效果和我們想要的一樣對嗎,可是這時候又存在一個bug:在你移動完成后,試試點擊邊欄的視圖吧——無論你怎么點擊,程序都不會響應。
iOS的事件分發是個有趣的機制,在之前我曾經寫過一篇文章講解scrollView的實現原理。里面提到了scrollView通過將layer的maskToBounds設為yes來實現隱藏可視范圍外的視圖渲染。但是,即使我們將這個值設為no之后,讓scrollView上所有的控件都進行渲染顯示之后,在其frame外的視圖同樣無法響應交互事件。
這兩個問題如出一轍,這是由于響應鏈導致的,在這里博主就不多做解釋,看官們只要先明白,超出父視圖frame范圍的子視圖正常情況下是無法響應交互事件的。
由于我們將邊欄視圖加入到當前的主視圖上,主視圖發生移動后,雖然成功顯示了邊欄的視圖,但是由于邊欄視圖處于主視圖的frame之外,因此無法接收我們的點擊事件。因此,上面的代碼可以說是失敗的。
解決方法
對于側滑移動后,邊欄視圖不響應點擊事件,我們有兩種方式可以解決這個問題
重寫主視圖的事件分發方法,在我們點擊的時候返回側欄視圖作為響應對象(常用于不規則形狀點擊響應)
-
使用障眼法。在點擊時進行截屏,將截屏圖片加到主視圖上。同時移動截屏的圖片和側欄視圖
兩種方法中,重寫事件分發相對復雜,容易出bug。但是功能強大,可擴展性極強;而障眼法簡單,無擴展性可言
博主使用的是第二種方式,障眼法。在iOS7之后UIView里面有一個方法用來返回當前顯示的內容狀態public func snapshotViewAfterScreenUpdates(afterUpdates: Bool) -> UIView
在動畫開始前,我們通過這個方法獲取主視圖當前的狀態截圖,并且加入到當前視圖上,和側滑視圖進行移動。并且將單擊還原手勢加在這個截圖的view上,也可以避免另外的事件點擊bug
var menuFrame: CGRect = (kMenuController?.view.frame)!
var mainFrame: CGRect = (kMainController?.view.frame)!
snapView = (kMainController?.view.snapshotViewAfterScreenUpdates(false))!
snapView!.frame = (kMainController?.view.frame)!
self.mainController?.view.addSubview(snapView!)
menuFrame.origin.x = (1 - maxOpenRatio) * kWidth
mainFrame.origin.x = -kWidth * maxOpenRatio
UIView.animateWithDuration(0.2, animations: { () -> Void in
self.snapView?.frame = mainFrame
self.menuController?.view.frame = menuFrame
}, completion: { (finished: Bool) -> Void in
if finished == true {
self.snapView?.addGestureRecognizer(self.tap!) }
})
同樣的,在我們關閉側欄視圖的動畫結束時,也應該把這個截圖的view從主視圖上移除
var menuFrame: CGRect = (kMenuController?.view.frame)!
var mainFrame: CGRect = (kMainController?.view.frame)!
mainFrame.origin.x = 0
if slideType == .MenuMove || demoType == .OnWindow {
menuFrame.origin.x = kWidth
}
UIView.animateWithDuration(0.2, animations: { () -> Void in
self.snapView?.frame = mainFrame
menuFrame.origin.x = kWidth
self.enuController?.view.frame = menuFrame
}, completion: { (finished: Bool) -> Void in
if finished {
self.snapView?.removeFromSuperview() }
})
其他
實現側滑功能存在著包括在內的循環引用的風險,為了避免這些風險,我們可以將管理側滑業務的功能封裝出來成為一個單例類,并且使用全局/靜態的對象指針來指向主視圖控制器跟側欄視圖控制器。
private static let let_sharedManager: LXDSlideManager = LXDSlideManager()
class func sharedManager() -> LXDSlideManager {
return let_sharedManager
}
private override init() {
super.init()
}
在swift中使用單例的時候,我們需要將構造器私有化,避免對象被創建。另外,我們還可以用不同的枚舉來表示不同的策劃效果
public enum LXDSlideMenuType: Int {
case BothMove //主界面跟側滑界面共同移動
case MenuMove //只移動側滑菜單界面
}
苦逼的博主從上周開始,不得不踏入swift的深坑之中,邊學swift邊進行項目開發。這段時間使用swift一來,給我的感覺是swift相當的簡潔,代碼量更少。當然了,靈活性比不得Objective-C。這是一門值得我們去學習挖掘潛力的語言。本文demo
文集:iOS開發
轉載注明鏈接:側滑界面的小實驗