本人剛陸續(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ù)覽圖

下面來(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)。
- TabTitleView 和 PageContentView 均繼承
UICollectionViewDelegate,并且分別在init方法中接收一個(gè)閉包 callback,用于回調(diào)更新UI。 - TabTitleView 重寫
collectionView(_: UICollectionView, didSelectItemAt: IndexPath)方法,在里面調(diào)用 callback 更新 PageContentView ,實(shí)現(xiàn)setOffset(offsetRatio: CGFloat)方法用于回調(diào)。 - PageContentView 重寫
scrollViewDidScroll(_: UIScrollView),在里面調(diào)用 callback 更新 TabTitleView,實(shí)現(xiàn)setPage(index: Int)方法用于回調(diào)。 - 在 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í)告訴我,十分感謝!