|
//
// PageViewController.swift
// PaiAi
//
// Created by ffib on 2018/12/6.
// Copyright © 2018 yb. All rights reserved.
//
import UIKit
struct PageOption {
var font: UIFont
var normalColor: UIColor
var selectedColor: UIColor
var spacing: CGFloat
init(font: UIFont = UIFont.systemFont(ofSize: 15),
normalColor: UIColor = UIColor(white: 1, alpha: 0.7),
selectedColor: UIColor = UIColor.white,
spacing: CGFloat = 10) {
self.font = font
self.normalColor = normalColor
self.selectedColor = selectedColor
self.spacing = spacing
}
}
struct PageItem {
var title: String
var viewController: UIViewController
init(title: String, viewController: UIViewController) {
self.title = title
self.viewController = viewController
}
}
fileprivate let baseTag = 55161750
class PageViewController: UIViewController {
private enum ScrollDirection {
case left
case right
}
private var scrollView = UIScrollView()
private var menuView = UIView()
private var sliderView = UIView()
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?
var option = PageOption()
var pageItems = [PageItem]() {
didSet {
configurationPageItems()
configurationMenuItems()
}
}
override func viewDidLoad() {
super.viewDidLoad()
contentRect = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
configurationScrollView()
configurationMenuView()
configurationSliderView()
}
override func viewDidLayoutSubviews() {
cacheMenuItemWidth()
}
fileprivate 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
}
}
fileprivate func configurationScrollView() {
scrollView.bounces = false
scrollView.delegate = self
scrollView.frame = contentRect
scrollView.isPagingEnabled = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
view.addSubview(scrollView)
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}
}
fileprivate func configurationMenuView() {
guard let barContentView = navigationController?.navigationBar else { return }
barContentView.addSubview(menuView)
menuView.translatesAutoresizingMaskIntoConstraints = false
let top = menuView.topAnchor
.constraint(equalTo: barContentView.topAnchor)
let bottom = menuView.bottomAnchor
.constraint(equalTo: barContentView.bottomAnchor)
let centerX = menuView.centerXAnchor
.constraint(equalTo: barContentView.centerXAnchor)
NSLayoutConstraint.activate([top, bottom, centerX])
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMenu(tap:)))
menuView.addGestureRecognizer(tap)
}
fileprivate func configurationSliderView() {
sliderView.backgroundColor = option.selectedColor
sliderView.translatesAutoresizingMaskIntoConstraints = false
sliderView.layer.cornerRadius = 2.5
menuView.addSubview(sliderView)
let width = sliderView.widthAnchor.constraint(equalToConstant: 5)
let height = sliderView.heightAnchor.constraint(equalToConstant: 5)
let bottom = sliderView.bottomAnchor
.constraint(equalTo: menuView.bottomAnchor, constant: -2)
NSLayoutConstraint.activate([width, height, bottom])
}
fileprivate func configurationPageItems() {
scrollView.contentSize = CGSize(width: contentRect.width * CGFloat(pageItems.count),
height: contentRect.height)
for (i, item) in pageItems.enumerated() {
addChildViewController(item.viewController)
scrollView.addSubview(item.viewController.view)
item.viewController.view.translatesAutoresizingMaskIntoConstraints = false
let width = item.viewController.view
.widthAnchor
.constraint(equalToConstant: contentRect.width)
let heigth = item.viewController.view
.heightAnchor
.constraint(equalToConstant: contentRect.height)
let top = item.viewController.view
.topAnchor
.constraint(equalTo: scrollView.topAnchor, constant: 0)
let leading = item.viewController.view
.leadingAnchor
.constraint(equalTo: scrollView.leadingAnchor, constant: CGFloat(i) * contentRect.width)
NSLayoutConstraint.activate([width, heigth, top, leading])
}
}
fileprivate func configurationMenuItems() {
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
}
configurationSliderViewDetail()
}
fileprivate func configurationSliderViewDetail() {
guard let label = menuView.viewWithTag(baseTag) else { return }
sliderConstraint = sliderView.centerXAnchor
.constraint(equalTo: label.centerXAnchor)
NSLayoutConstraint.activate([sliderConstraint!])
}
}
//action
extension PageViewController {
@objc fileprivate 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 {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startOffset = self.scrollView.contentOffset.x
isBeginScroll = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking else { return }
initializeScrollParameter()
moveSlider(percentage: (self.scrollView.contentOffset.x - startOffset ) / contentRect.width)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
guard !decelerate else { return }
pageViewDidEndScroll()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
pageViewDidEndScroll()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
pageViewDidEndScroll()
}
}
//linkage
extension PageViewController {
fileprivate func didSelect(_ index: Int) {
startOffset = self.scrollView.contentOffset.x
sliderAnimation(index)
didSelectPageItem(index)
}
fileprivate func initializeScrollParameter() {
if isBeginScroll {
direction = self.scrollView.contentOffset.x - startOffset > 0 ? .right : .left
guard let label = currentItem else { return }
switch direction {
case .left:
distance = menuItemWidths[label.tag - baseTag] / 2 + menuItemWidths[label.tag - baseTag - 1] / 2 + option.spacing
case .right:
distance = menuItemWidths[label.tag - baseTag] / 2 + menuItemWidths[label.tag - baseTag + 1] / 2 + option.spacing
}
isBeginScroll = false
}
}
}
//menu
extension PageViewController {
fileprivate func moveSlider(percentage: CGFloat) {
sliderConstraint?.constant = percentage * distance
}
fileprivate 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()
}
fileprivate 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()
}
}
fileprivate func updateSliderConstraint() {
NSLayoutConstraint.deactivate([sliderConstraint!])
sliderConstraint = sliderView.centerXAnchor.constraint(equalTo: currentItem!.centerXAnchor)
NSLayoutConstraint.activate([sliderConstraint!])
}
}
//page
extension PageViewController {
fileprivate func didSelectPageItem(_ index: Int) {
scrollView.setContentOffset(CGPoint.init(x: CGFloat(index) * contentRect.width, y: 0), animated: true)
}
fileprivate func pageViewDidEndScroll() {
guard self.scrollView.contentOffset.x != startOffset else { return }
let index = Int(self.scrollView.contentOffset.x / contentRect.width)
switch direction {
case .left:
didSelectMenuItem(index)
case .right:
didSelectMenuItem(index)
}
}
}
|