作者: ZeroJian
在很多新聞咨詢類 App 中, 經(jīng)常會(huì)使用一個(gè)滾動(dòng)視圖來展示各種分類信息, 使用 ScrollView 或 CollectionView 添加多個(gè) View, 他們都在一個(gè)視圖控制器中, 通過滾動(dòng)達(dá)到切換頁面的目的, 這種視圖的結(jié)構(gòu)和布局大概是這樣:
通過在視圖控制器添加一層滾動(dòng)視圖擴(kuò)充它的 contentSize 達(dá)到切換內(nèi)容的目的
這種視圖結(jié)構(gòu)在處理單層頁面時(shí)很方便, 但是如果某些場(chǎng)景需要處理多層視圖結(jié)構(gòu)如何處理呢, 例如很多打車軟件, 它們都有一個(gè)主視圖控制器,底層顯示著地圖,根據(jù)業(yè)務(wù)功能的不同顯示不同的視圖元素,點(diǎn)擊打車或一些按鈕會(huì)跳轉(zhuǎn)到第二層視圖, 但是它們的視圖控制器都還是在主視圖控制器中
這種視圖結(jié)構(gòu)很適合使用 棧 來實(shí)現(xiàn), 我們回顧一下系統(tǒng) UINavigationController 的實(shí)現(xiàn)方式, UINavigationController通過 Navigation Stack 來管理 View controller,對(duì)View進(jìn)行push/pop:
Page Stack的實(shí)現(xiàn)
我們參照系統(tǒng)的 UINavigationController 實(shí)現(xiàn)一個(gè)控制 View 的導(dǎo)航控制器, Page Stack
首先創(chuàng)建一個(gè)類, 定義一些基礎(chǔ)屬性
public enum StackViewNavigationOperation: Int {
case none
case push
case pop
}
class StackViewNavigation {
var views: [UIView] = []
var isAnimation: Bool = false
var topView: UIView
var rootView: UIView
// 用來臨時(shí)保存上一個(gè) topView
var lastTopView: UIView
/// UIViewController 的 View, 所有視圖都添加到此視圖上
var superView: UIView
init(viewController:UIViewController, rootView: UIView) {
self.superView = viewController.view
self.superViewHeight = viewController.view.bounds.height
self.rootView = rootView
self.topView = rootView
self.lastTopView = rootView
views.append(rootView)
}
}
考慮到擴(kuò)展性, 我們可以定義一個(gè)視圖切換動(dòng)畫協(xié)議來處理視圖的切換
class StackViewNavigation: ViewSwitchAnimation {
...
}
protocol ViewSwitchAnimation {
var superView: UIView { get set }
var superViewHeight: CGFloat { get set }
/// 記錄動(dòng)畫中的 closure, 便于外部監(jiān)聽動(dòng)畫中事件
var nextViewAnimationAction: ((UIView, Bool) -> Void)? { get set }
/// 設(shè)置切換的視圖布局, 便于更改視圖高度
var bottomInster: CGFloat { get set }
var topInster: CGFloat { get set }
}
extension ViewSwitchAnimation {
/// 視圖切換動(dòng)畫
func animation(operation: StackViewNavigationOperation, topView: UIView, nextView: UIView, animated: Bool,completion: ((Bool) -> Void)?) {
....
/// 動(dòng)畫結(jié)束 closure
completion?(finished)
}
}
我們實(shí)現(xiàn)視圖的一些棧操作, 類似系統(tǒng)的 UINavigationController
func pushView(_ view: UIView, animated: Bool) -> Bool {
let success = viewAnimation(operation: .push, nextView: view, animated: animated) { (finished) in
if finished {
self.views.append(view)
}
}
return success
}
func popView(animated: Bool) -> UIView? {
guard views.count - 2 >= 0 else {
print("popView failure, 已經(jīng)是 rootView")
return nil
}
let nextView = views[views.count - 2]
let success = viewAnimation(operation: .pop, nextView: nextView, animated: animated) { (finished) in
self.views.removeLast()
}
return success ? topView : nil
}
func popToView(_ view: UIView, animated: Bool) -> [UIView]? {
guard let index = views.index(of: view) else {
print("PopToView failure, 無法找到當(dāng)前 view")
return nil
}
guard views.last != view else {
print("PopToView failure, 和當(dāng)前 topView 是同一個(gè) view")
return nil
}
let viewsSlice = views[0...index]
let popedSlice = views[(index + 1)...]
let success = viewAnimation(operation: .pop, nextView: view, animated: animated) { (finished) in
if finished {
self.views = Array(viewsSlice)
}
}
return success ? Array(popedSlice) : nil
}
func popToRootView(animated: Bool) -> [UIView]? {
guard rootView != topView else {
print("popToRootView failure, 和當(dāng)前 topView 是同一個(gè) view")
return nil
}
var popedView = views
popedView.removeFirst()
let success = viewAnimation(operation: .pop, nextView: rootView, animated: animated) { (finished) in
if finished {
self.views = [self.rootView]
}
}
return success ? popedView : nil
}
/// 彈出視圖到一個(gè)新的 stack root view
///
/// - Parameters:
/// - view: view
/// - animated: 動(dòng)畫
/// - Returns: 返回上一個(gè) rootView 和 彈出的 views
func popToNewRootView(view: UIView, animated: Bool) -> (UIView?, [UIView]?) {
guard view != topView else {
print("popToNewRootView failure, 和當(dāng)前 topView 是同一個(gè) view")
return (nil, nil)
}
guard !isAnimation else {
print("popToNewRootView failure, 前一個(gè)動(dòng)畫還未結(jié)束")
return (nil, nil)
}
rootView = view
let lastRootView = views.removeFirst()
views.insert(view, at: 0)
return (lastRootView, popToRootView(animated: animated))
}
我們完成了基礎(chǔ)的視圖棧操作, 包括 Push, Pop, PopToView, PopToRootView, 它的功能和 UINavigationController 一致, 只是它在一個(gè)視圖控制器中, 控制顯示在視圖控制器的視圖, 我們可以擴(kuò)展下它的方法, 比如某層棧視圖的橫向切換, 例如滴滴業(yè)務(wù)視圖 StackPage1 的左右切換
/// 使用 ViewSwitchAnimation 協(xié)議
public class NavigationRoute: ViewSwitchAnimation {
...
/// StackViewNavigation 屬性
fileprivate var stackViewNavigation: StackViewNavigation
public init(viewController:UIViewController, rootView: UIView) {
stackViewNavigation = StackViewNavigation(rootView: rootView)
/// 協(xié)議屬性
self.superView = viewController.view
self.superViewHeight = viewController.view.bounds.height
}
/// stackViewNaviation 的一些方法
public func pushView(_ view: UIView, animated: Bool) -> Bool {
return stackViewNavigation.pushView(view, animated: animated)
}
public func popToRootView(animated: Bool) -> Bool {
return stackViewNavigation.popToRootView(animated: animated) != nil ? true : false
}
public func popView(animated: Bool) -> Bool {
return stackViewNavigation.popView(animated: animated) != nil ? true : false
}
public func popToView(_ view: UIView, animated: Bool) -> Bool {
return stackViewNavigation.popToView(view, animated: animated) != nil ? true : false
}
}
通過 NavigationRoute 封裝了一層, 我們可以寫一些代理方法來記錄視圖切換的一些事件, 并實(shí)現(xiàn)橫向切換
public protocol NavigationRouteDelegate: class {
/// 當(dāng)前棧頂視圖將要顯示
func navigationRoute(route: NavigationRoute, nextViewDidShow nextView: UIView, topView: UIView)
/// 當(dāng)前 top 視圖將要隱藏
func navigationRoute(route: NavigationRoute, topViewWillHidden topView: UIView, nextView: UIView)
/// 視圖切換動(dòng)畫中
func navigationRoute(route: NavigationRoute, nextViewAnimation nextView: UIView, animated: Bool)
}
/// 橫向切換視圖, pop: 切換到左邊動(dòng)畫, push: 切換到右邊的動(dòng)畫
public func switchTopView(direction: StackViewNavigationOperation, nextView: UIView, animated: Bool) -> Bool {
guard !stackViewNavigation.isAnimation else {
print("switchTopView failure, 前一個(gè)動(dòng)畫還未結(jié)束")
return false
}
guard stackViewNavigation.topView != nextView else {
print("switchTopView failure, nextView 和 topView 是同一個(gè) view")
return false
}
delegate?.navigationRoute(route: self, topViewWillHidden: stackViewNavigation.topView, nextView: nextView)
stackViewNavigation.isAnimation = true
animation(operation: direction, topView: stackViewNavigation.topView, nextView: nextView, animated: animated) { (finished) in
self.stackViewNavigation.topView = nextView
self.stackViewNavigation.isAnimation = false
self.stackViewNavigation.views.removeLast()
// 防止更換的 topView 是 rootView
if self.stackViewNavigation.views.count == 0 {
self.stackViewNavigation.rootView = nextView
}
self.stackViewNavigation.views.append(nextView)
let lastTopView = self.stackViewNavigation.lastTopView
self.delegate?.navigationRoute(route: self, nextViewDidShow: nextView, topView: lastTopView)
self.stackViewNavigation.lastTopView = nextView
}
return true
}
至此, 我們實(shí)現(xiàn)了一個(gè)視圖棧切換功能, switchTopView 方法當(dāng)然也可以通過傳遞一個(gè) UIScrollView 或 UICollectionView 來實(shí)現(xiàn), 但是這樣的話監(jiān)聽視圖切換事件的代理可能就需要外部調(diào)用實(shí)現(xiàn), 如果使用內(nèi)部的 switchTopView 方法, 我們可以把視圖切換的所有事件都在內(nèi)部監(jiān)聽便于外部使用
看看效果:
使用
/// 設(shè)置 RootView, 添加到 ViewController 的 view 中完成布局
let rootView = UIView()
view.addSubview(rootView)
rootView.snp.makeConstraints { (maker) in
maker.top.equalToSuperview().inset(64)
maker.left.right.bottom.equalToSuperview()
}
/// 初始化 NavigationRoute, 并設(shè)置 topInster 和 bottomInster
navigationRoute = NavigationRoute(viewController: self, rootView: rootView)
navigationRoute.topInster = 64
navigationRoute.setupDelegate()
navigationRoute.delegate = self
/// 視圖切換的一些方法
navigationRoute.pushView(view, animated: animated)
navigationRoute.popView(animated: animated)
navigationRoute.popToRootView(animated: animated)
navigationRoute.switchTopView(direction: .push, nextView: view, animated: animated)
/// 代理方法
func navigationRoute(route: NavigationRoute, nextViewDidShow nextView: UIView, topView: UIView) {
print("nextViewDidShow")
}
func navigationRoute(route: NavigationRoute, topViewWillHidden topView: UIView, nextView: UIView) {
/// 可設(shè)置切換后的視圖布局
nextView.topInster = 100
naxtView.bottomInster = 50
print("topViewWillHidden")
}
func navigationRoute(route: NavigationRoute, nextViewAnimation nextView: UIView, animated: Bool) {
/// 切換視圖動(dòng)畫中... 可把其他聯(lián)動(dòng)動(dòng)畫放在這里
print("nextViewAnimation")
}
這還可以擴(kuò)展很多方法, 比如上下的切換, 自定義動(dòng)畫, 有興趣可以自己改造下
項(xiàng)目代碼已經(jīng)放到 github: https://github.com/ZeroJian/NavigationRoute