使用[棧]結(jié)構(gòu)完成一個(gè)頁面切換控制器 Page Stack

作者: ZeroJian

在很多新聞咨詢類 App 中, 經(jīng)常會(huì)使用一個(gè)滾動(dòng)視圖來展示各種分類信息, 使用 ScrollView 或 CollectionView 添加多個(gè) View, 他們都在一個(gè)視圖控制器中, 通過滾動(dòng)達(dá)到切換頁面的目的, 這種視圖的結(jié)構(gòu)和布局大概是這樣:

toutiaoCollectionView

通過在視圖控制器添加一層滾動(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)到第二層視圖, 但是它們的視圖控制器都還是在主視圖控制器中

DiDIStackPage

這種視圖結(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)聽便于外部使用

看看效果:

DiDIStackPage

使用

/// 設(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市租幕,隨后出現(xiàn)的幾起案子引瀑,更是在濱河造成了極大的恐慌豺撑,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡俏扩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門弊添,熙熙樓的掌柜王于貴愁眉苦臉地迎上來录淡,“玉大人,你說我怎么就攤上這事油坝〖灯荩” “怎么了刨裆?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長彼水。 經(jīng)常有香客問我崔拥,道長极舔,這世上最難降的妖魔是什么凤覆? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮拆魏,結(jié)果婚禮上盯桦,老公的妹妹穿的比我還像新娘。我一直安慰自己渤刃,他們只是感情好拥峦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著卖子,像睡著了一般略号。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上洋闽,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天玄柠,我揣著相機(jī)與錄音,去河邊找鬼诫舅。 笑死羽利,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刊懈。 我是一名探鬼主播这弧,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼虚汛!你這毒婦竟也來了匾浪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤卷哩,失蹤者是張志新(化名)和其女友劉穎蛋辈,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體殉疼,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梯浪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瓢娜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挂洛。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖眠砾,靈堂內(nèi)的尸體忽然破棺而出虏劲,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布柒巫,位于F島的核電站励堡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏堡掏。R本人自食惡果不足惜应结,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望泉唁。 院中可真熱鬧鹅龄,春花似錦、人聲如沸亭畜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拴鸵。三九已至玷坠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間劲藐,已是汗流浹背八堡。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瘩燥,地道東北人秕重。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像厉膀,于是被迫代替她去往敵國和親溶耘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1服鹅、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,981評(píng)論 3 119
  • 我看到昏黃的田野 冷漠的夕陽 以及飛馳的工廠 我看到或者看不到熙熙攘攘 我看到許多人跟我一樣或者不一樣 我看到銀色...
    onlyxy閱讀 304評(píng)論 0 0
  • 陽光凳兵,天高氣爽, 很美好的早晨企软,做早飯庐扫,孩子吃飽了,送孩子去上學(xué)仗哨,回來又精心的給先生準(zhǔn)備了更豐盛的早餐形庭!吃...
    蟬心安閱讀 907評(píng)論 7 3
  • 不是每天的頭腦都是很清醒的,總有很糊涂的時(shí)候厌漂,這個(gè)時(shí)候要對(duì)自己寬容一點(diǎn):)
    Bruceshaoshao閱讀 124評(píng)論 0 0
  • 生活過的城市對(duì)比:深圳 VS 昆明 (沒有絕對(duì)性萨醒,相對(duì)而言) 1,職場(chǎng):深圳的職場(chǎng)人普遍更愿意實(shí)際能拿多少錢苇倡,不在...
    乾小龍閱讀 188評(píng)論 0 0