123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- import UIKit
- private let baseTag = 55161750
- open class PageViewController: UIViewController {
- private enum ScrollDirection {
- case left
- case right
- }
- private var contentRect = CGRect.zero
-
- private var currentIndex: Int = 0
- private var distance: CGFloat = 0
- private var currentItem: UILabel?
- private var startOffset: CGFloat = 0
- private var isBeginScroll: Bool = false
- private var menuItemWidths: [CGFloat] = []
- private var direction: ScrollDirection = .right
- private var sliderConstraint: NSLayoutConstraint?
- public private(set) lazy var scrollView: UIScrollView = {
- let scrollView = UIScrollView()
- scrollView.bounces = false
- scrollView.delegate = self
- scrollView.isPagingEnabled = true
- scrollView.alwaysBounceVertical = false
- scrollView.showsVerticalScrollIndicator = false
- scrollView.showsHorizontalScrollIndicator = false
- return scrollView
- }()
- public private(set) lazy var menuView: UIView = {
- let view = UIView()
- return view
- }()
- public private(set) lazy var sliderView: UIView = {
- let view = UIView()
- view.layer.cornerRadius = 2.5
- view.backgroundColor = option.selectedColor
- return view
- }()
- public var option = PageOption()
- public var pageItems = [PageItem]() {
- didSet {
- DispatchQueue.main.async {
- self.setPageItems()
- self.setMenuItems()
- }
- }
- }
- override open func viewDidLoad() {
- super.viewDidLoad()
- contentRect = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
- if #available(iOS 11, *) {
- scrollView.contentInsetAdjustmentBehavior = .never
- }
- constructViewHierarchy()
- activateConstraints()
- setMenuGestureRecognizer()
- }
- open override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- cacheMenuItemWidth()
- }
- private func cacheMenuItemWidth() {
- guard menuItemWidths.isEmpty else { return }
- menuItemWidths = Array(repeating: 0, count: menuView.subviews.count - 1)
- for view in menuView.subviews {
- guard let label = view as? UILabel else { continue }
- menuItemWidths[label.tag - baseTag] = label.frame.width
- }
- }
- private func constructViewHierarchy() {
- navigationItem.titleView = menuView
- view.addSubview(scrollView)
- menuView.addSubview(sliderView)
- }
- }
- fileprivate extension PageViewController {
- func setPageItems() {
- guard !pageItems.isEmpty else { return }
- var navigationBarHeight: CGFloat
- if UIApplication.shared.statusBarFrame.height == 44 {
- navigationBarHeight = 88
- } else {
- navigationBarHeight = 64
- }
- scrollView.contentSize = CGSize(width: contentRect.width * CGFloat(pageItems.count),
- height: contentRect.height - navigationBarHeight)
- var last: UIView?
- for item in pageItems {
- addChild(item.viewController)
- scrollView.addSubview(item.viewController.view)
- item.viewController.view.translatesAutoresizingMaskIntoConstraints = false
- let width = item.viewController.view
- .widthAnchor
- .constraint(equalTo: scrollView.widthAnchor)
- let height = item.viewController.view
- .heightAnchor
- .constraint(equalTo: scrollView.heightAnchor)
- let top = item.viewController.view
- .topAnchor
- .constraint(equalTo: scrollView.topAnchor)
- let leading = item.viewController.view
- .leadingAnchor
- .constraint(equalTo: last?.trailingAnchor ?? scrollView.leadingAnchor)
- NSLayoutConstraint.activate([width, height, top, leading])
- last = item.viewController.view
- }
- }
- func setMenuItems() {
- guard !pageItems.isEmpty else { return }
- var last: UILabel?
- var width: CGFloat = 0
- for (i, item) in pageItems.enumerated() {
- let label = UILabel()
- label.text = item.title
- label.tag = baseTag + i
- label.font = option.font
- label.textAlignment = .center
- label.textColor = option.normalColor
- label.translatesAutoresizingMaskIntoConstraints = false
- menuView.addSubview(label)
- if i == 0 {
- label.textColor = option.selectedColor
- label.font = UIFont.systemFont(ofSize: option.font.pointSize + 1)
- currentItem = label
- } else if i == pageItems.count - 1 {
- NSLayoutConstraint.activate([label.trailingAnchor
- .constraint(equalTo: menuView.trailingAnchor)])
- }
- NSLayoutConstraint.activate([
- label.leadingAnchor.constraint(equalTo: last?.trailingAnchor ?? menuView.leadingAnchor,
- constant: last == nil ? 0 : option.spacing),
- label.centerYAnchor.constraint(equalTo: menuView.centerYAnchor)])
- last = label
- if #available(iOS 11, *) { continue }
- label.sizeToFit()
- width += label.frame.width
- }
- setSliderViewDetail()
- if #available(iOS 11, *) { return }
- menuView.frame.size = CGSize(width: width + CGFloat(pageItems.count - 1) * option.spacing, height: 40)
- }
- func setSliderViewDetail() {
- guard let label = menuView.viewWithTag(baseTag) else { return }
- sliderConstraint = sliderView.centerXAnchor.constraint(equalTo: label.centerXAnchor)
- NSLayoutConstraint.activate([sliderConstraint!])
- if #available(iOS 11, *) {
- NSLayoutConstraint.activate([sliderView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 6)])
- }
- }
- }
- fileprivate extension PageViewController {
- func activateConstraints() {
- activateConstraintsSliderView()
- activateConstraintsScrollView()
- }
- func activateConstraintsScrollView() {
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- let top = scrollView.topAnchor.constraint(equalTo: view.topAnchor)
- let width = scrollView.widthAnchor.constraint(equalTo: view.widthAnchor)
- let bottom = scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- let height = scrollView.heightAnchor.constraint(equalTo: view.heightAnchor)
- let leading = scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
- let trailing = scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
- NSLayoutConstraint.activate([width, height, top, leading, bottom, trailing])
- }
- func activateConstraintsSliderView() {
- sliderView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- sliderView.widthAnchor.constraint(equalToConstant: 5),
- sliderView.heightAnchor.constraint(equalToConstant: 5),
- sliderView.bottomAnchor.constraint(equalTo: menuView.bottomAnchor, constant: -2)
- ])
- }
- }
- fileprivate extension PageViewController {
- func setMenuGestureRecognizer() {
- let tap = UITapGestureRecognizer(target: self, action: #selector(tapMenu(tap:)))
- menuView.addGestureRecognizer(tap)
- }
- @objc func tapMenu(tap: UITapGestureRecognizer) {
- var x = tap.location(in: menuView).x
- for (i, width) in menuItemWidths.enumerated() {
- x -= (width + option.spacing / 2)
- guard x <= 0 else { continue }
- if i != currentIndex {
- didSelect(i)
- }
- return
- }
- }
- }
- extension PageViewController: UIScrollViewDelegate {
- public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
- startOffset = self.scrollView.contentOffset.x
- isBeginScroll = true
- }
- public func scrollViewDidScroll(_ scrollView: UIScrollView) {
- guard scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking else { return }
- initializeScrollParameter()
- moveSlider(percentage: (self.scrollView.contentOffset.x - startOffset ) / contentRect.width)
- }
- public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
- guard !decelerate else { return }
- pageViewDidEndScroll()
- }
- public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
- pageViewDidEndScroll()
- }
- public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
- pageViewDidEndScroll()
- }
- }
- fileprivate extension PageViewController {
- func didSelect(_ index: Int) {
- startOffset = self.scrollView.contentOffset.x
- sliderAnimation(index)
- didSelectPageItem(index)
- }
- func initializeScrollParameter() {
- if isBeginScroll {
- direction = self.scrollView.contentOffset.x - startOffset > 0 ? .right : .left
- guard let label = currentItem else { return }
- let curr = menuItemWidths[label.tag - baseTag]
- switch direction {
- case .left:
- guard label.tag - baseTag - 1 > 0 else { return }
- distance = curr / 2 + menuItemWidths[label.tag - baseTag - 1] / 2 + option.spacing
- case .right:
- guard label.tag - baseTag + 1 < menuItemWidths.count else { return }
- distance = curr / 2 + menuItemWidths[label.tag - baseTag + 1] / 2 + option.spacing
- }
- isBeginScroll = false
- }
- }
- }
- fileprivate extension PageViewController {
- func moveSlider(percentage: CGFloat) {
- sliderConstraint?.constant = percentage * distance
- }
- func didSelectMenuItem(_ index: Int) {
- guard let currentLabel = currentItem,
- let label = menuView.viewWithTag(baseTag + index) as? UILabel else { return }
- currentItem = label
- currentLabel.font = option.font
- currentLabel.textColor = option.normalColor
- label.textColor = option.selectedColor
- label.font = UIFont.systemFont(ofSize: option.font.pointSize + 1)
- currentIndex = index
- updateSliderConstraint()
- }
- func sliderAnimation(_ index: Int) {
- let isLeft = currentIndex - index > 0
- var animationDistance: CGFloat = 0
- for i in isLeft ? (index..<currentIndex) : (currentIndex..<index) {
- animationDistance += menuItemWidths[i] / 2 + menuItemWidths[i + 1] / 2 + option.spacing
- }
- UIView.animate(withDuration: 0.25) {
- self.sliderConstraint?.constant = isLeft ? -animationDistance : animationDistance
- self.menuView.layoutIfNeeded()
- }
- }
- func updateSliderConstraint() {
- NSLayoutConstraint.deactivate([sliderConstraint!])
- sliderConstraint = sliderView.centerXAnchor.constraint(equalTo: currentItem!.centerXAnchor)
- NSLayoutConstraint.activate([sliderConstraint!])
- }
- }
- fileprivate extension PageViewController {
- func didSelectPageItem(_ index: Int) {
- scrollView.setContentOffset(CGPoint.init(x: CGFloat(index) * contentRect.width, y: 0), animated: true)
- }
- func pageViewDidEndScroll() {
- guard self.scrollView.contentOffset.x != startOffset else { return }
- let index = Int(self.scrollView.contentOffset.x / contentRect.width)
- pageItems[index].viewController.didMove(toParent: self)
- switch direction {
- case .left:
- didSelectMenuItem(index)
- case .right:
- didSelectMenuItem(index)
- }
- }
- }
|