|
//
// 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)
}
}
}
|