暫無描述

PageViewController.swift 12KB

    // // PageViewController.swift // PaiAi // // Created by ffib on 2018/12/6. // Copyright © 2018 yb. All rights reserved. // import UIKit fileprivate let baseTag = 55161750 open class PageViewController: UIViewController { private enum ScrollDirection { case left case right } private var contentRect = CGRect.zero //animation auxiliary 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 { setPageItems() 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) } } /// set PageItem and MenuItem 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? 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) let left: NSLayoutConstraint if let lastLabel = last { left = label.leftAnchor.constraint(equalTo: lastLabel.rightAnchor, constant: option.spacing) } else { left = label.leadingAnchor.constraint(equalTo: menuView.leadingAnchor) } let centerY = label.centerYAnchor.constraint(equalTo: menuView.centerYAnchor) 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([left, centerY]) last = label } setSliderViewDetail() } func setSliderViewDetail() { guard let label = menuView.viewWithTag(baseTag) else { return } sliderConstraint = sliderView.centerXAnchor.constraint(equalTo: label.centerXAnchor) NSLayoutConstraint.activate([sliderConstraint!, sliderView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 6)]) } } /// layout 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) ]) } } /// GuestureRecognizer 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 } } } /// UIScrollViewDelegate implementation 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() } } /// linkage 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 } switch direction { case .left: guard label.tag - baseTag - 1 > 0 else { return } distance = menuItemWidths[label.tag - baseTag] / 2 + menuItemWidths[label.tag - baseTag - 1] / 2 + option.spacing case .right: guard label.tag - baseTag + 1 < menuItemWidths.count else { return } distance = menuItemWidths[label.tag - baseTag] / 2 + menuItemWidths[label.tag - baseTag + 1] / 2 + option.spacing } isBeginScroll = false } } } /// menu linkage 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!]) } } /// page linkage 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) } } }