实现功能:
- 简单调用实现多页面滚动控制。
- 子页面数量自适应。
- 滚动结束代理回调index。
实现思路:
这个控制器由两部分部分组成。第一部分是上面子标题scrollView,第二部分是下方显示子控制器scrollView。
使用EWPageController类将两个scrollView添加并通过代理绑定起来。
再将外部设置以及滚动结果通过代理暴露给继承的子控制器实现页面设置。
实现方式:
为上方标题scrollView定义UI参数枚举。
实现上方标题scrollView。
实现下方控制器scrollView。
添加两个scrollView的代理方法。
添加外部设置代理方法,暴露给继承的子控制器进行设置。
实现EWPageController将两个scrollView添加,实现相应代理方法实现两个scrollView的动作绑定。并添加外部override代理方法。
1. 为上方标题scrollView定义UI参数枚举
/// 上方滚动Bar参数
public enum EWViewPageIndicatorBarOption {
/// bar高度
case height(CGFloat)
/// bar背景色
case backgroundColor(UIColor)
/// bar左侧padding
case barPaddingleft(CGFloat)
/// bar右侧padding
case barPaddingRight(CGFloat)
/// bar上方padding
case barPaddingTop(CGFloat)
/// bar标题normal字体
case barItemTitleFont(UIFont)
/// bar标题选中字体
case barItemTitleSelectedFont(UIFont)
/// bar标题颜色
case barItemTitleColor(UIColor)
/// bar标题选中颜色
case barItemTitleSelectedColor(UIColor)
/// 选中滑块颜色
case indicatorColor(UIColor)
/// 选中滑块高度
case indicatorHeight(CGFloat)
/// 选中滑块距离bar底部高度
case indicatorBottom(CGFloat)
/// bar下分割线颜色
case bottomlineColor(UIColor)
/// bar下分割线高度
case bottomlineHeight(CGFloat)
/// bar下分割线左padding
case bottomlinePaddingLeft(CGFloat)
/// bar下分割线右padding
case bottomlinePaddingRight(CGFloat)
}
2.实现上方标题scrollView
/// 上方滚动bar
class EWViewPageIndicatorBar: UIView {
fileprivate weak var delegate: EWViewpageIndicatorBarDelegate?
private let contentView = UIScrollView()
/// 滑块
private let indicatorContainer = UIView() ///和barItem一样宽的透明View
private let indicator = UIView() /// 用户可见的滚动View
private var indicatorColor = UIColor.gray
private var indicatorTitles = [String]()
private var indicatorBackgroundColor = UIColor.white
private var indicatorHeight: CGFloat = 8.0
private var indicatorBottom: CGFloat = 0.0
/// Bar底部线
private let bottomline = UIView()
private var bottomlineColor = UIColor.blue
private var bottomlineHeight: CGFloat = 5.0
private var bottomlinePaddingLeft: CGFloat = 0.0
private var bottomlinePaddingRight: CGFloat = 0.0
/// Bar本身的属性
private var barHeight: CGFloat = 50.0
private var paddingLeft: CGFloat = 0.0
private var paddingRight: CGFloat = 0.0
private var paddingTop: CGFloat = 0.0
/// BarItem
private var barItemTitleFont = UIFont.systemFont(ofSize: 17)
private var barItemTitleSelectedFont = UIFont.systemFont(ofSize: 17)
private var barItemWidth: CGFloat = 100.0
private var barItemTitleColor = UIColor.black
private var barItemTitleSelectedColor = UIColor.blue
private var buttonItems = [EWViewPageIndicatorBarButtonItem]()
private var curIndex = 0
private var itemCount = 0
func setUp(with options: [EWViewPageIndicatorBarOption], titles: [String]) {
parse(options: options, itemCount: titles.count)
setUpUIElement(with: titles)
}
/// 根据传来的参数配置滚动Bar
private func parse(options: [EWViewPageIndicatorBarOption], itemCount: Int) {
for option in options {
switch (option) {
case let .height(value):
self.barHeight = value
case let .backgroundColor(value):
self.backgroundColor = value
case let .barPaddingleft(value):
self.paddingLeft = value
case let .barPaddingRight(value):
self.paddingRight = value
case let .barPaddingTop(value):
self.paddingTop = value
case let .barItemTitleFont(value):
self.barItemTitleFont = value
case let .barItemTitleSelectedFont(value):
self.barItemTitleSelectedFont = value
case let .barItemTitleColor(value):
self.barItemTitleColor = value
case let .barItemTitleSelectedColor(value):
self.barItemTitleSelectedColor = value
case let .indicatorColor(value):
self.indicatorColor = value
case let .indicatorHeight(value):
self.indicatorHeight = value
case let .indicatorBottom(value):
self.indicatorBottom = value
case let .bottomlineColor(value):
self.bottomlineColor = value
case let .bottomlineHeight(value):
self.bottomlineHeight = value
case let .bottomlinePaddingLeft(value):
self.bottomlinePaddingLeft = value
case let .bottomlinePaddingRight(value):
self.bottomlinePaddingRight = value
}
}
self.itemCount = itemCount
/// barItemWidth自适应
self.barItemWidth = (EWScreenInfo.Width-paddingLeft-paddingRight)/CGFloat(itemCount)
}
private func setUpUIElement(with titles: [String]) {
self.addSubview(contentView)
contentView.frame = CGRect(x: paddingLeft, y: paddingTop, width: UIScreen.main.bounds.width-paddingLeft-paddingRight, height: barHeight-paddingTop)
contentView.backgroundColor = UIColor.clear
self.frame = CGRect(x: 0, y: EWScreenInfo.navigationHeight, width: EWScreenInfo.Width, height: barHeight)
contentView.contentSize = CGSize(width: barItemWidth*CGFloat(titles.count), height: barHeight-paddingTop)
for (index, title) in titles.enumerated() {
let buttonItem = EWViewPageIndicatorBarButtonItem()
buttonItem.backgroundColor = UIColor.clear
buttonItem.titleLabel?.font = barItemTitleFont
buttonItem.setTitle(title, for: .normal)
buttonItem.titleLabel?.textAlignment = .center
buttonItem.titleLabel?.sizeToFit()
buttonItem.setTitleColor(barItemTitleColor, for: .normal)
buttonItem.tag = index
buttonItem.frame = CGRect(x: CGFloat(index)*barItemWidth, y: 0, width: barItemWidth, height: barHeight-paddingTop)
buttonItem.addTarget(self, action: #selector(onClickTitle(_:)), for: .touchUpInside)
buttonItems.append(buttonItem)
contentView.addSubview(buttonItem)
}
bottomline.frame = CGRect(x: bottomlinePaddingLeft,
y: barHeight - bottomlineHeight,
width: EWScreenInfo.Width - bottomlinePaddingLeft - bottomlinePaddingRight,
height: bottomlineHeight / UIScreen.main.scale * 2)
bottomline.backgroundColor = bottomlineColor
self.addSubview(bottomline)
indicatorContainer.frame = CGRect(x: 0,
y: barHeight - paddingTop - 6 - indicatorBottom,
width: barItemWidth,
height: indicatorHeight)
indicatorContainer.addSubview(indicator)
indicator.backgroundColor = indicatorColor
contentView.addSubview(indicatorContainer)
self.setNeedsLayout()
self.layoutIfNeeded()
}
/// 点击bar上title
@objc private func onClickTitle(_ title: UIControl) {
let index = Int(title.tag)
self.delegate?.didClickedIndicatorItem(index: index)
scrollIndicator(to: index)
}
/// 外部方法,滚动至目标位置
fileprivate func scrollIndicator(to index: Int, animated: Bool = true) {
let range = 0..
3.实现下方控制器scrollView
/// pageView的ScrollView
class EWPageScrollView: UIScrollView {
private var _pages = [EWPage]()
fileprivate var pages: [EWPage] {
return _pages
}
fileprivate func setup(with pages: [EWPage]) {
_pages = pages
self.contentSize = CGSize(width: CGFloat(pages.count) * (self.frame.width), height: 0)
for (index , page) in pages.enumerated() {
page.view.frame = CGRect(x: CGFloat(index)*self.frame.width, y: 0, width: self.frame.width, height: self.frame.height)
}
}
/// 滚动
fileprivate func scrollToPage(index: Int, animation: Bool = true) {
guard index < pages.count else { return }
if animation {
UIView.animate(withDuration: 0.2) {
self.contentOffset = CGPoint(x: CGFloat(index)*self.pages[index].view.frame.width, y: 0)
}
} else {
self.contentOffset = CGPoint(x: CGFloat(index)*self.pages[index].view.frame.width, y: 0)
}
}
}
4. 添加两个scrollView的代理方法
/// 为滚动bar上的scrollview添加delegate,获取bar的滚动状态
private class EWPageScrollViewDelegate: NSObject, UIScrollViewDelegate {
weak var scrollView: UIScrollView?
/// scrollView当前展示左侧x位置
var startLeft: CGFloat = 0.0
/// scrollView当前展示右侧x位置
var startRight: CGFloat = 0.0
/// 当scrollView滚动到最左侧
var whenScrollToLeftEdge: (() -> Void)?
/// 当scrollView滚动到最右侧
var whenScrollToRightEdge: (() -> Void)?
/// 当scrollView滚动某一page
var whenScrollToPageIndex: ((_ index: Int) -> Void)?
/// scrollView开始滚动,UIScrollViewDelegate中的方法
fileprivate func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard self.scrollView == scrollView else { return }
/// 记录scrollView初始位置
startLeft = scrollView.contentOffset.x
startRight = scrollView.contentOffset.x + scrollView.frame.size.width
}
/// scrollView滚动减速, UIScrollViewDelegate中的方法
fileprivate func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard self.scrollView == scrollView else { return }
/// 获取滚动结束时右侧边的x
let lastEdge = scrollView.contentOffset.x + scrollView.frame.size.width
if (lastEdge == scrollView.contentSize.width && lastEdge == startRight) {
/// 如果滚动结束时lastEdge等于scrollView.contentSize.width且 lastEdge等于startRight。相当于scrollView已经滚动到了最右边,并且这次操作并有没有滚动
self.whenScrollToRightEdge?()
} else if (scrollView.contentOffset.x == 0 && startLeft == 0) {
/// 如果滚动结束时scrollView.contentOffset.x == 0且 startLeft == 0。相当于scrollView已经滚动到了最左边,并且这次操作并有没有滚动
self.whenScrollToLeftEdge?()
} else {
/// 正常滚动中根据 scrollView.contentOffset.x来获取选取页的index
self.whenScrollToPageIndex?(Int(scrollView.contentOffset.x/scrollView.frame.size.width))
}
}
}
/// 点击滚动bar标题delegate方法
protocol EWViewpageIndicatorBarDelegate: class {
func didClickedIndicatorItem(index: Int)
}
5.添加外部设置代理方法,暴露给继承的子控制器进行设置
/// 外部继承delegate方法
protocol EWViewPageDelegate: class {
/// 子控制器title数组
func titles(for viewpage: EWPageScrollView) -> [String]
/// 子控制器titleBar设置UI参数
func options(for viewpage: EWPageScrollView) -> [EWViewPageIndicatorBarOption]?
/// 子控制器页面
func pages(for viewPage: EWPageScrollView) -> [EWPage]
/// 当前滚动至页面index
func didScrollToPage(index: Int)
/// 滚动至最左边控制器后仍左滑
func didScrollToLeftEdge()
/// 滚动至最右边控制器后仍右滑
func didScrollToRightEdge()
}
6. 实现EWPageController将两个scrollView添加,并实现相应代理方法实现两个scrollView的动作绑定
class EWPageViewController: UIViewController, EWViewPageDelegate, EWViewpageIndicatorBarDelegate {
private var _titles = [String]()
var titles: [String] {
return _titles
}
private var _viewPage: EWPageScrollView! = nil
var viewPage : EWPageScrollView {
return _viewPage
}
/// 选中index
private var _curIndex = 0
var curIndex : Int {
set(newValue) {
_curIndex = newValue
}
get {
return _curIndex
}
}
private let scrollDelegate = EWPageScrollViewDelegate()
private var indicatorBar = EWViewPageIndicatorBar()
/// 通过这个属性保证滚动滑块的显示
private var autoScrollIndicator = true
var scrollEnable = true
override func viewDidLoad() {
super.viewDidLoad()
_curIndex = defaultPageIndex()
self.setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.setNeedsLayout()
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.indicatorBar.scrollIndicator(to: curIndex, animated: false)
viewPage.scrollToPage(index: curIndex)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
autoScrollIndicator = false
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
/// 第一次load页面时调用方法加载滚动滑块
if autoScrollIndicator {
self.indicatorBar.scrollIndicator(to: curIndex, animated: false)
}
}
private func setupUI() {
_viewPage = EWPageScrollView()
_viewPage.bounces = false
_viewPage.isScrollEnabled = scrollEnable
self._titles = self.titles(for: self._viewPage)
if let options = self.options(for: self._viewPage) {
self.indicatorBar.setUp(with: options, titles: titles)
}
self.indicatorBar.delegate = self
self.view.addSubview(indicatorBar)
let viewPageFrame = CGRect(x: 0,
y: self.indicatorBar.frame.origin.y + self.indicatorBar.frame.height,
width: self.view.frame.width,
height: self.view.frame.height - EWScreenInfo.navigationHeight - self.indicatorBar.frame.height)
_viewPage.frame = viewPageFrame
_viewPage.setup(with: self.pages(for: self._viewPage))
_viewPage.pages.forEach({ viewPage.addSubview($0.view); self.addChild($0)})
_viewPage.scrollToPage(index: _curIndex, animation: false)
scrollDelegate.scrollView = _viewPage
_viewPage.delegate = scrollDelegate
_viewPage.isPagingEnabled = true
_viewPage.showsHorizontalScrollIndicator = false
self.view.addSubview(_viewPage)
self.scrollDelegate.whenScrollToLeftEdge = { [weak self] in
self?.didScrollToLeftEdge()
}
self.scrollDelegate.whenScrollToRightEdge = { [weak self] in
self?.didScrollToRightEdge()
}
self.scrollDelegate.whenScrollToPageIndex = { [weak self] index in
self?._curIndex = index
self?.didScrollToPage(index: index)
self?._viewPage.scrollToPage(index: index)
self?.indicatorBar.scrollIndicator(to: index)
}
}
/// 点击上方滑动barItem
func didClickedIndicatorItem(index: Int) {
_viewPage.scrollToPage(index: index)
self.didScrollToPage(index: index)
}
/// 默认index
func defaultPageIndex() -> Int {
return 0
}
// MARK: 外部调用方法,必须override
func titles(for viewpape: EWPageScrollView) -> [String] {
fatalError("请覆盖该方法")
}
func options(for viewpage: EWPageScrollView) -> [EWViewPageIndicatorBarOption]? {
fatalError("请覆盖该方法")
}
func pages(for viewPage: EWPageScrollView) -> [EWPage] {
fatalError("请覆盖该方法")
}
func didScrollToPage(index: Int) {
fatalError("请覆盖该方法")
}
func didScrollToLeftEdge() {
fatalError("请覆盖该方法")
}
func didScrollToRightEdge() {
fatalError("请覆盖该方法")
}
}
调用方法:
将EWPageViewController文件拖入项目,调用时新建控制器继承自EWPageViewController,实现必选代理方法:
/// 子控制器数量由下两个方法控制
/// 子控制器title
override func titles(for viewpage: EWPageScrollView) -> [String] {
return ["第一页","第二页","第三页","第四页"]
}
/// 子控制器
override func pages(for viewPage: EWPageScrollView) -> [EWPage] {
return [EWSubViewController(text: "第一页"),EWSubViewController(text: "第二页"),EWSubViewController(text: "第三页"),EWSubViewController(text: "第四页")]
}
/// 子控制器UI设置
override func options(for viewpage: EWPageScrollView) -> [EWViewPageIndicatorBarOption]? {
let pageOptions: [EWViewPageIndicatorBarOption] = [
.height(52),
.backgroundColor(UIColor.white),
.barPaddingleft(0),
.barPaddingRight(0),
.barItemTitleFont(UIFont.systemFont(ofSize: 15)),
.barItemTitleSelectedFont(UIFont.boldSystemFont(ofSize: 15)),
.barItemTitleColor(UIColor.lightGray),
.barItemTitleSelectedColor(UIColor.black),
.indicatorColor(UIColor.red),
.indicatorHeight(2),
.indicatorBottom(5),
.bottomlineColor(UIColor.brown),
.bottomlineHeight(0)
]
return pageOptions
}
/// 当前显示子控制器index
override func didScrollToPage(index: Int) {
print(index)
}
/// 滚动至最左边控制器后仍左滑
override func didScrollToLeftEdge() {
print("left")
}
/// 滚动至最右边控制器后仍右滑
override func didScrollToRightEdge() {
print("right")
}
github地址: EWPageController
有问题欢迎探讨.