Swift.多页面滚动控制器

效果图

实现功能:

  • 简单调用实现多页面滚动控制。
  • 子页面数量自适应。
  • 滚动结束代理回调index。

实现思路:

这个控制器由两部分部分组成。第一部分是上面子标题scrollView,第二部分是下方显示子控制器scrollView。
使用EWPageController类将两个scrollView添加并通过代理绑定起来。
再将外部设置以及滚动结果通过代理暴露给继承的子控制器实现页面设置。

实现方式:

  1. 为上方标题scrollView定义UI参数枚举。

  2. 实现上方标题scrollView。

  3. 实现下方控制器scrollView。

  4. 添加两个scrollView的代理方法。

  5. 添加外部设置代理方法,暴露给继承的子控制器进行设置。

  6. 实现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

有问题欢迎探讨.

你可能感兴趣的:(Swift.多页面滚动控制器)