iOS 標(biāo)簽式滑動(dòng)分頁(yè)View - TabPageView 的實(shí)現(xiàn)思路

本人剛陸續(xù)學(xué)習(xí)了 iOS 開(kāi)發(fā)一個(gè)月左右,因?yàn)轳R上就要做一個(gè)簡(jiǎn)單的 iOS 項(xiàng)目,有個(gè)需求是需要實(shí)現(xiàn)類似于 Android中 PageView 的功能,而 github 上我又沒(méi)有找到合適項(xiàng)目可以直接拿來(lái)用,因此在參考了別人的一些代碼后,自己用 swift 寫了一個(gè)簡(jiǎn)單的 TabPageView。

先上預(yù)覽圖


TabPageViewDemo.gif

下面來(lái)說(shuō)說(shuō)原理。其實(shí)原理挺簡(jiǎn)單的,我是將它分成了兩個(gè)部分,分別是標(biāo)簽欄 TabTitleView 以及頁(yè)面內(nèi)容 PageContentView ,然后分別添加 CollectionView 來(lái)展示內(nèi)容。TabTitleView 底部添加一個(gè) UIView 作為滾動(dòng)條, PageContentView 中的 CollectionView 開(kāi)啟分頁(yè), 基本上內(nèi)容的展示就沒(méi)有問(wèn)題了,重要的是怎么讓它們聯(lián)動(dòng)。

  1. TabTitleView 和 PageContentView 均繼承 UICollectionViewDelegate,并且分別在init方法中接收一個(gè)閉包 callback,用于回調(diào)更新UI。
  2. TabTitleView 重寫 collectionView(_: UICollectionView, didSelectItemAt: IndexPath)方法,在里面調(diào)用 callback 更新 PageContentView ,實(shí)現(xiàn)setOffset(offsetRatio: CGFloat)方法用于回調(diào)。
  3. PageContentView 重寫scrollViewDidScroll(_: UIScrollView),在里面調(diào)用 callback 更新 TabTitleView,實(shí)現(xiàn)setPage(index: Int)方法用于回調(diào)。
  4. 在 View 的初始化過(guò)程中,接收 controllers,這些 controllers 的 view 作為 PageContentView 展示的內(nèi)容。初始化 TabTitleView,傳入一個(gè) callback,調(diào)用 PageContentView 的setPage(index: Int)方法;初始化 PageContentView,傳入一個(gè) callback,調(diào)用 TabTitleView 的setOffset(offsetRatio: CGFloat)方法。

聯(lián)動(dòng)的基本實(shí)現(xiàn)方式就是這樣了,接下來(lái)看看代碼。

TabPageViewController.swift
public func initView(titleList: [TabTitleModel], controllers: [UIViewController], contentView: UIView, configuration: TabPageViewConfiguration){
        var pageViewList = Array<UIView>()
        if controllers.count > 0 {
            for controller in controllers{
                pageViewList.append(controller.view)
                addChildViewController(controller)
                controllerList.append(controller)
            }
        }
        tabTitleView = TabTitleView(frame: CGRect(x: 0, y: 0, width: contentView.frame.width, height: configuration.titleHeight), titleList:
            titleList, conf: configuration.tabTitleConf, callback:{
                (index) in
                self.pageContentView?.setPage(index: index)
        })
        contentView.addSubview(tabTitleView!)
        pageContentView = PageContentView(frame: CGRect(x: 0, y: configuration.titleHeight, width: contentView.frame.width, height: contentView.frame.height - configuration.titleHeight), pageViewList: pageViewList, conf: configuration.pageViewConf, callback:{
            (offset) in
            self.tabTitleView?.setOffset(offsetRatio: offset)
        })
        contentView.addSubview(pageContentView!)
    }
TabTitleView.swift
    // MARK: methods
    func setOffset(offsetRatio: CGFloat){
        var offsetX = CGFloat()
        offsetX = 0
        let index = Int(offsetRatio)
        if index > 0{
            for i in 0...index - 1{
                offsetX += titleViewList![i].bounds.width
                offsetX += collectionLayout!.minimumInteritemSpacing
            }
        }
        offsetX += (titleViewList![index].bounds.width + collectionLayout!.minimumInteritemSpacing) * (offsetRatio - CGFloat(index))
        UIView.animate(withDuration: 0.0, animations: {
            var width: CGFloat
            if index < self.titleViewList!.count - 1{
                width = self.titleViewList![index].frame.width + (self.titleViewList![index + 1].frame.width - self.titleViewList![index].frame.width) * (offsetRatio - CGFloat(index))
            }else{
                width = self.titleViewList![index].frame.width
            }
            
            self.scrollBar!.frame = CGRect(x:offsetX, y:self.scrollBar!.frame.origin.y, width: width, height:self.scrollBar!.frame.height)
            
        })
        currentIndex = index
    }
    
    func setIndex(index: Int){
        var offsetX = CGFloat()
        offsetX = 0
        if let viewList = titleViewList, index > 0{
            for index in 0...index - 1{
                offsetX += viewList[index].bounds.width
                offsetX += collectionLayout!.minimumInteritemSpacing
            }
        }
        UIView.animate(withDuration: 0.3, animations: {
            
            self.scrollBar!.frame = CGRect(x:offsetX, y:self.scrollBar!.frame.origin.y, width:self.titleViewList![index].frame.width, height:self.scrollBar!.frame.height)
            
        })
        currentIndex = index
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if currentIndex != indexPath.row {
            setIndex(index: indexPath.row)
            callback!(indexPath.row)
            currentIndex = indexPath.row
        }
    }
PageContentView.swift
    // MARK: delegate
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if collectionView!.contentOffset.x == 0{
            callback?(0)
        }else if doCallbackInvock{
            let offsetRatio = collectionView!.contentOffset.x / self.frame.width
            callback?(offsetRatio)
        }
    }
    
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        doCallbackInvock = true
    }
    
    // MARK: methods
    func setPage(index: Int){
        let point = CGPoint(x: CGFloat(index) * collectionView!.frame.size.width, y:collectionView!.frame.origin.y)
        doCallbackInvock = false
        collectionView?.setContentOffset(point, animated: true)
        currentIndex = index
    }

調(diào)用該 TabPageView 的方式是,先讓 controller 繼承 TabPageViewController,然后像這樣調(diào)用initView方法

        let firstController = UIStoryboard.init(name: "FirstFragment", bundle: nil).instantiateViewController(withIdentifier: "FirstFragment")
        let secondController = UIStoryboard.init(name: "SecondFragment", bundle: nil).instantiateViewController(withIdentifier: "SecondFragment")
        let taskController = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Task")
        
        let image = UIImage(named: "logo")
        let titleList: [TabTitleModel] = [TabTitleModel("aaa", image!), TabTitleModel("bbbbbbbbb", image!), TabTitleModel("cccc", image!)]
        var conf = TabPageViewConfiguration()
        initView(titleList: titleList, controllers: [firstController, secondController, taskController], contentView: contentView, configuration: conf)

實(shí)現(xiàn)起來(lái)還是比較簡(jiǎn)單的,也基本滿足了我新項(xiàng)目的需求。
順便把這個(gè)項(xiàng)目的 github 地址也貼上:https://github.com/beetlebum233/iOS-TabPageView
如果有更好的實(shí)現(xiàn)思路或者有可以優(yōu)化的地方,可以隨時(shí)告訴我,十分感謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明AI閱讀 16,171評(píng)論 3 119
  • 顯示數(shù)據(jù)https://hacking-with-angular.github.io/2016/08/03/dis...
    4ea0af17fd67閱讀 138評(píng)論 0 0
  • 記在上海找工作的第一天 不曾想過(guò),找工作是那么悲慘的一件事情。跑了四五個(gè)律所,既荒涼又小,不似心中高大上的律所。位...
    小星星_c003閱讀 203評(píng)論 0 0
  • 1. 動(dòng)態(tài)內(nèi)存分配的意義 (1)C語(yǔ)言中的一切操作都是基于內(nèi)存的(2) 變量和數(shù)組都是內(nèi)存的別名 內(nèi)存分配由編譯器...
    編程半島閱讀 673評(píng)論 0 0
  • 今天我來(lái)介紹我的弟弟,他喜歡看電視和吃東西,他現(xiàn)在再看超級(jí)飛俠,有時(shí)候他要看電視,我們不給他看他就哭,有時(shí)候他很早...
    高子皓閱讀 192評(píng)論 0 0

友情鏈接更多精彩內(nèi)容