本文会实现一个轻量级的 PageMenu, 用户可以点击按钮或者滑动视图切换各个 ViewController,点击的按钮会放在 titleView 内。导入到项目的时候只需要添加一个类就OK了。
ScrollView
我们知道 ScrollView 有一个属性 contentSize,它表示的是 ScrollView 的内容可滚动区域,在实现的时候,如果滚动视图有 n 个, 则可设置:
scrollView.contentSize = CGSize(width: scrollView.bounds.width * n, height: scrollView.bounds.height)
由于切换各个视图的时候需要一个分页的动画效果,所以需要设置 ScrollView
scrollView.isPagingEnabled = true
此时,假如都设置好了的话,就可以在各个视图来回滑动切换了,就是这么简单!
点击切换按钮
点击切换按钮的时候,ScrollView 需要滚动到相对应的视图,此时需要修改的是 ScrollView 的 contentOffset 属性:
scrollView.setContentOffset(viewControllersFrame[index].origin, animated: true)
提示动画条
当用户滑动到第 n 个界面的时候,此时提示条应该移动到第 n 个界面对应的点击按钮
此时需要在 滑动视图 和 点击按钮 时做相应的处理
- 滑动视图时的处理:
在 ScrollView 的代理方法内,根据对应按钮的 frame 比例,执行滚动动画
func scrollViewDidScroll(_ scrollView: UIScrollView) {
UIView.animate(withDuration: moveDuration, animations: {
let x = scrollView.contentOffset.x * self.scale + self.itemsOriginX[0]
self.indicatorView.frame.origin.x = x
})
}
- 点击按钮时的处理
在点击按钮添加的方法内,添加滚动动画:
UIView.animate(withDuration: moveDuration, animations: {
self.indicatorView.frame.origin.x = self.itemsOriginX[index]
})
我的实现类:
最终实现的滚动容器类代码如下:
import UIKit
class MenuContainerViewController: UIViewController {
var menus: [UIButton] = [UIButton]()
var viewControllers: [UIViewController] = [UIViewController]()
var itemColor = UIColor.black
var indicatorColor = UIColor.blue {
didSet {
indicatorView?.backgroundColor = indicatorColor
}
}
// designated init view controller
init(menus: [UIButton], viewControllers: [UIViewController]) {
super.init(nibName: nil, bundle: nil)
if menus.count != viewControllers.count {
fatalError("menus.count != viewControllers.count")
}
self.menus = menus
self.viewControllers = viewControllers
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Private properties
private var scrollView: UIScrollView!
private var itemsTitle: [String] = [String]()
private var viewControllersFrame: [CGRect] = [CGRect]()
fileprivate var itemsOriginX: [CGFloat] = [CGFloat]()
fileprivate var indicatorView: UIView!
fileprivate var indicatorViewLastOriginX: CGFloat = 0.0 {
didSet {
indicatorCopyView?.frame.origin.x = indicatorViewLastOriginX
}
}
fileprivate let indicatorViewWidth: CGFloat = 30
fileprivate var scale: CGFloat!
fileprivate let moveDuration: TimeInterval = 0.2
// Due to 'sectionIndicatorView' will reset frame when viewDidDisappear did called,
// so, add 'indicatorCopyView' as the copy view
fileprivate var indicatorCopyView: UIView!
fileprivate var shouldAdjustCopyIndicatorView = false
// MARK: - View controller lifecycle
override func viewDidLoad() {
super.viewDidLoad()
automaticallyAdjustsScrollViewInsets = false
self.navigationController?.navigationBar.isTranslucent = false
scrollView = UIScrollView()
let customTitleView = UIView()
let titleStackView = UIStackView()
indicatorView = UIView()
indicatorCopyView = UIView()
scrollView.delegate = self
scrollView.isPagingEnabled = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
for button in menus {
button.setTitleColor(itemColor, for: .normal)
itemsTitle.append(button.currentTitle!)
}
customTitleView.backgroundColor = UIColor.white
for item in menus {
titleStackView.addArrangedSubview(item)
}
titleStackView.alignment = .center
titleStackView.axis = .horizontal
titleStackView.distribution = .fillEqually
for i in 0 ..< viewControllers.count {
let subvc = viewControllers[i]
self.addChildViewController(subvc)
scrollView.addSubview(subvc.view)
subvc.didMove(toParentViewController: self)
}
let titleViewWidth: CGFloat = 200
let titleViewHeight: CGFloat = 44
let stackViewHeight: CGFloat = 40
let titleViewFrame = CGRect(x: 0, y: 0, width: titleViewWidth, height: titleViewHeight)
let stackViewFrame = CGRect(x: 0, y: 0, width: titleViewWidth, height: stackViewHeight)
let indicatorViewFrame = CGRect(x: 0, y: titleViewHeight - 2, width: indicatorViewWidth, height: 1)
customTitleView.frame = titleViewFrame
customTitleView.frame.origin.x = self.view.frame.midX - titleViewWidth/2
titleStackView.frame = stackViewFrame
indicatorView.frame = indicatorViewFrame
indicatorView.backgroundColor = indicatorColor
// for menuItems originX
var itemOriginX: CGFloat = 0
let itemWidth: CGFloat = titleViewWidth/3
for item in menus {
item.addTarget(self, action: #selector(contentOffSetXForButton(sender:)), for: .touchUpInside)
let itemFrame = CGRect(x: itemOriginX, y: 0, width: itemWidth, height: stackViewHeight)
item.frame = itemFrame
let indicatorOriginX = itemFrame.midX - indicatorViewWidth/2
itemsOriginX.append(indicatorOriginX)
itemOriginX += itemWidth
}
// for sectionIndicatorView
indicatorView.frame.origin.x = itemsOriginX[0]
indicatorViewLastOriginX = indicatorView.frame.origin.x
// indicator copy view
indicatorCopyView.frame = indicatorView.frame
indicatorCopyView.backgroundColor = indicatorView.backgroundColor
indicatorCopyView.isHidden = true
// indicator scroll scale
let indicatorScale = itemsOriginX[1] - itemsOriginX[0]
scale = indicatorScale / UIScreen.main.bounds.size.width
customTitleView.addSubview(titleStackView)
customTitleView.addSubview(indicatorView)
customTitleView.addSubview(indicatorCopyView)
self.parent?.navigationItem.titleView = customTitleView
view.addSubview(scrollView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var scrollViewFrame = view.frame
scrollViewFrame.size.height -= 49
scrollView.frame = view.frame
let width = scrollViewFrame.width
let height = scrollViewFrame.height
scrollView.contentSize = CGSize(width: width * 3, height: height)
// has [viewControllersFrame]
var vcOriginX: CGFloat = 0
for _ in 0 ..< viewControllers.count {
viewControllersFrame.append(CGRect(x: vcOriginX, y: 0, width: width, height: height))
vcOriginX += width
}
for i in 0 ..< viewControllers.count {
viewControllers[i].view.frame = viewControllersFrame[i]
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if shouldAdjustCopyIndicatorView {
UIView.animate(withDuration: 0.0, animations: {
self.indicatorView?.frame.origin.x = self.indicatorViewLastOriginX
}) { (_) in
self.indicatorCopyView?.isHidden = true
self.indicatorView?.isHidden = false
self.shouldAdjustCopyIndicatorView = false
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
indicatorCopyView.isHidden = false
indicatorView.isHidden = true
shouldAdjustCopyIndicatorView = true
}
// MARK: - Helper
@objc private func contentOffSetXForButton(sender: UIButton){
let currentTitle = sender.currentTitle!
let index = itemsTitle.index(of: currentTitle)!
scrollView.setContentOffset(viewControllersFrame[index].origin, animated: true)
UIView.animate(withDuration: moveDuration, animations: {
self.indicatorView.frame.origin.x = self.itemsOriginX[index]
self.indicatorViewLastOriginX = self.indicatorView.frame.origin.x
})
}
}
// MRAK: - Scroll view delegate
extension MenuContainerViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
UIView.animate(withDuration: moveDuration, animations: {
let x = scrollView.contentOffset.x * self.scale + self.itemsOriginX[0]
self.indicatorView.frame.origin.x = x
self.indicatorViewLastOriginX = x
})
}
}
调用的测试类:
import UIKit
class ViewController: UIViewController {
// *********************************************
// Add MenuContainerViewController like this
private var addedPageViewController = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !addedPageViewController {
addedPageViewController = true
let scrollContainerVC = MenuContainerViewController(menus: pageItems(),
viewControllers: pageViewContorllers())
self.addChildViewController(scrollContainerVC)
scrollContainerVC.view.frame = view.bounds
view.addSubview(scrollContainerVC.view)
scrollContainerVC.didMove(toParentViewController: self)
}
}
// *********************************************
// MRAK: Test data
private func pageItems() -> [UIButton] {
let red = UIButton()
let gray = UIButton()
let purple = UIButton()
red.setTitle("red", for: .normal)
gray.setTitle("gray", for: .normal)
purple.setTitle("purple", for: .normal)
var items = [UIButton]()
items.append(red)
items.append(gray)
items.append(purple)
return items
}
private func pageViewContorllers() -> [UIViewController] {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let firstViewController = storyboard.instantiateViewController(withIdentifier: "FirstViewController") as!
FirstViewController
let secondViewController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
let thirdViewController = storyboard.instantiateViewController(withIdentifier: "ThirdViewController") as!
ThirdViewController
var vcs = [UIViewController]()
vcs.append(firstViewController)
vcs.append(secondViewController)
vcs.append(thirdViewController)
return vcs
}
}