Nessuna descrizione

PageViewController.swift 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. //
  2. // PageViewController.swift
  3. // PaiAi
  4. //
  5. // Created by ffib on 2018/12/6.
  6. // Copyright © 2018 yb. All rights reserved.
  7. //
  8. import UIKit
  9. struct PageOption {
  10. var font: UIFont
  11. var normalColor: UIColor
  12. var selectedColor: UIColor
  13. var spacing: CGFloat
  14. init(font: UIFont = UIFont.systemFont(ofSize: 15),
  15. normalColor: UIColor = UIColor(white: 1, alpha: 0.7),
  16. selectedColor: UIColor = UIColor.white,
  17. spacing: CGFloat = 10) {
  18. self.font = font
  19. self.normalColor = normalColor
  20. self.selectedColor = selectedColor
  21. self.spacing = spacing
  22. }
  23. }
  24. struct PageItem {
  25. var title: String
  26. var viewController: UIViewController
  27. init(title: String, viewController: UIViewController) {
  28. self.title = title
  29. self.viewController = viewController
  30. }
  31. }
  32. fileprivate let baseTag = 55161750
  33. class PageViewController: UIViewController {
  34. private enum ScrollDirection {
  35. case left
  36. case right
  37. }
  38. private var scrollView = UIScrollView()
  39. private var menuView = UIView()
  40. private var sliderView = UIView()
  41. private var contentRect = CGRect.zero
  42. //animation auxiliary
  43. private var currentIndex: Int = 0
  44. private var distance: CGFloat = 0
  45. private var currentItem: UILabel?
  46. private var startOffset: CGFloat = 0
  47. private var isBeginScroll: Bool = false
  48. private var menuItemWidths: [CGFloat] = []
  49. private var direction: ScrollDirection = .right
  50. private var sliderConstraint: NSLayoutConstraint?
  51. var option = PageOption()
  52. var pageItems = [PageItem]() {
  53. didSet {
  54. configurationPageItems()
  55. configurationMenuItems()
  56. }
  57. }
  58. override func viewDidLoad() {
  59. super.viewDidLoad()
  60. contentRect = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
  61. configurationScrollView()
  62. configurationMenuView()
  63. configurationSliderView()
  64. }
  65. override func viewDidLayoutSubviews() {
  66. cacheMenuItemWidth()
  67. }
  68. fileprivate func cacheMenuItemWidth() {
  69. guard menuItemWidths.isEmpty else { return }
  70. menuItemWidths = Array(repeating: 0, count: menuView.subviews.count - 1)
  71. for view in menuView.subviews {
  72. guard let label = view as? UILabel else { continue }
  73. menuItemWidths[label.tag - baseTag] = label.frame.width
  74. }
  75. }
  76. fileprivate func configurationScrollView() {
  77. scrollView.bounces = false
  78. scrollView.delegate = self
  79. scrollView.frame = contentRect
  80. scrollView.isPagingEnabled = true
  81. scrollView.showsVerticalScrollIndicator = false
  82. scrollView.showsHorizontalScrollIndicator = false
  83. view.addSubview(scrollView)
  84. if #available(iOS 11.0, *) {
  85. scrollView.contentInsetAdjustmentBehavior = .never
  86. } else {
  87. automaticallyAdjustsScrollViewInsets = false
  88. }
  89. }
  90. fileprivate func configurationMenuView() {
  91. guard let barContentView = navigationController?.navigationBar else { return }
  92. barContentView.addSubview(menuView)
  93. menuView.translatesAutoresizingMaskIntoConstraints = false
  94. let top = menuView.topAnchor
  95. .constraint(equalTo: barContentView.topAnchor)
  96. let bottom = menuView.bottomAnchor
  97. .constraint(equalTo: barContentView.bottomAnchor)
  98. let centerX = menuView.centerXAnchor
  99. .constraint(equalTo: barContentView.centerXAnchor)
  100. NSLayoutConstraint.activate([top, bottom, centerX])
  101. let tap = UITapGestureRecognizer(target: self, action: #selector(tapMenu(tap:)))
  102. menuView.addGestureRecognizer(tap)
  103. }
  104. fileprivate func configurationSliderView() {
  105. sliderView.backgroundColor = option.selectedColor
  106. sliderView.translatesAutoresizingMaskIntoConstraints = false
  107. sliderView.layer.cornerRadius = 2.5
  108. menuView.addSubview(sliderView)
  109. let width = sliderView.widthAnchor.constraint(equalToConstant: 5)
  110. let height = sliderView.heightAnchor.constraint(equalToConstant: 5)
  111. let bottom = sliderView.bottomAnchor
  112. .constraint(equalTo: menuView.bottomAnchor, constant: -2)
  113. NSLayoutConstraint.activate([width, height, bottom])
  114. }
  115. fileprivate func configurationPageItems() {
  116. scrollView.contentSize = CGSize(width: contentRect.width * CGFloat(pageItems.count),
  117. height: contentRect.height)
  118. for (i, item) in pageItems.enumerated() {
  119. addChildViewController(item.viewController)
  120. scrollView.addSubview(item.viewController.view)
  121. item.viewController.view.translatesAutoresizingMaskIntoConstraints = false
  122. let width = item.viewController.view
  123. .widthAnchor
  124. .constraint(equalToConstant: contentRect.width)
  125. let heigth = item.viewController.view
  126. .heightAnchor
  127. .constraint(equalToConstant: contentRect.height)
  128. let top = item.viewController.view
  129. .topAnchor
  130. .constraint(equalTo: scrollView.topAnchor, constant: 0)
  131. let leading = item.viewController.view
  132. .leadingAnchor
  133. .constraint(equalTo: scrollView.leadingAnchor, constant: CGFloat(i) * contentRect.width)
  134. NSLayoutConstraint.activate([width, heigth, top, leading])
  135. }
  136. }
  137. fileprivate func configurationMenuItems() {
  138. var last: UILabel?
  139. for (i, item) in pageItems.enumerated() {
  140. let label = UILabel()
  141. label.text = item.title
  142. label.tag = baseTag + i
  143. label.font = option.font
  144. label.textAlignment = .center
  145. label.textColor = option.normalColor
  146. label.translatesAutoresizingMaskIntoConstraints = false
  147. menuView.addSubview(label)
  148. let left: NSLayoutConstraint
  149. if let lastLabel = last {
  150. left = label.leftAnchor
  151. .constraint(equalTo: lastLabel.rightAnchor, constant: option.spacing)
  152. } else {
  153. left = label.leadingAnchor
  154. .constraint(equalTo: menuView.leadingAnchor)
  155. }
  156. let centerY = label.centerYAnchor
  157. .constraint(equalTo: menuView.centerYAnchor)
  158. if i == 0 {
  159. label.textColor = option.selectedColor
  160. label.font = UIFont.systemFont(ofSize: option.font.pointSize + 1)
  161. currentItem = label
  162. } else if i == pageItems.count - 1 {
  163. NSLayoutConstraint.activate([label.trailingAnchor
  164. .constraint(equalTo: menuView.trailingAnchor)])
  165. }
  166. NSLayoutConstraint.activate([left, centerY])
  167. last = label
  168. }
  169. configurationSliderViewDetail()
  170. }
  171. fileprivate func configurationSliderViewDetail() {
  172. guard let label = menuView.viewWithTag(baseTag) else { return }
  173. sliderConstraint = sliderView.centerXAnchor
  174. .constraint(equalTo: label.centerXAnchor)
  175. NSLayoutConstraint.activate([sliderConstraint!])
  176. }
  177. }
  178. //action
  179. extension PageViewController {
  180. @objc fileprivate func tapMenu(tap: UITapGestureRecognizer) {
  181. var x = tap.location(in: menuView).x
  182. for (i, width) in menuItemWidths.enumerated() {
  183. x -= (width + option.spacing / 2)
  184. guard x <= 0 else { continue }
  185. if i != currentIndex {
  186. didSelect(i)
  187. }
  188. return
  189. }
  190. }
  191. }
  192. extension PageViewController: UIScrollViewDelegate {
  193. func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  194. startOffset = self.scrollView.contentOffset.x
  195. isBeginScroll = true
  196. }
  197. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  198. guard scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking else { return }
  199. initializeScrollParameter()
  200. moveSlider(percentage: (self.scrollView.contentOffset.x - startOffset ) / contentRect.width)
  201. }
  202. func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  203. guard !decelerate else { return }
  204. pageViewDidEndScroll()
  205. }
  206. func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  207. pageViewDidEndScroll()
  208. }
  209. func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  210. pageViewDidEndScroll()
  211. }
  212. }
  213. //linkage
  214. extension PageViewController {
  215. fileprivate func didSelect(_ index: Int) {
  216. startOffset = self.scrollView.contentOffset.x
  217. sliderAnimation(index)
  218. didSelectPageItem(index)
  219. }
  220. fileprivate func initializeScrollParameter() {
  221. if isBeginScroll {
  222. direction = self.scrollView.contentOffset.x - startOffset > 0 ? .right : .left
  223. guard let label = currentItem else { return }
  224. switch direction {
  225. case .left:
  226. distance = menuItemWidths[label.tag - baseTag] / 2 + menuItemWidths[label.tag - baseTag - 1] / 2 + option.spacing
  227. case .right:
  228. distance = menuItemWidths[label.tag - baseTag] / 2 + menuItemWidths[label.tag - baseTag + 1] / 2 + option.spacing
  229. }
  230. isBeginScroll = false
  231. }
  232. }
  233. }
  234. //menu
  235. extension PageViewController {
  236. fileprivate func moveSlider(percentage: CGFloat) {
  237. sliderConstraint?.constant = percentage * distance
  238. }
  239. fileprivate func didSelectMenuItem(_ index: Int) {
  240. guard let currentLabel = currentItem,
  241. let label = menuView.viewWithTag(baseTag + index) as? UILabel else { return }
  242. currentItem = label
  243. currentLabel.font = option.font
  244. currentLabel.textColor = option.normalColor
  245. label.textColor = option.selectedColor
  246. label.font = UIFont.systemFont(ofSize: option.font.pointSize + 1)
  247. currentIndex = index
  248. updateSliderConstraint()
  249. }
  250. fileprivate func sliderAnimation(_ index: Int) {
  251. let isLeft = currentIndex - index > 0
  252. var animationDistance: CGFloat = 0
  253. for i in isLeft ? (index..<currentIndex) : (currentIndex..<index) {
  254. animationDistance += menuItemWidths[i] / 2 + menuItemWidths[i + 1] / 2 + option.spacing
  255. }
  256. UIView.animate(withDuration: 0.25) {
  257. self.sliderConstraint?.constant = isLeft ? -animationDistance : animationDistance
  258. self.menuView.layoutIfNeeded()
  259. }
  260. }
  261. fileprivate func updateSliderConstraint() {
  262. NSLayoutConstraint.deactivate([sliderConstraint!])
  263. sliderConstraint = sliderView.centerXAnchor.constraint(equalTo: currentItem!.centerXAnchor)
  264. NSLayoutConstraint.activate([sliderConstraint!])
  265. }
  266. }
  267. //page
  268. extension PageViewController {
  269. fileprivate func didSelectPageItem(_ index: Int) {
  270. scrollView.setContentOffset(CGPoint.init(x: CGFloat(index) * contentRect.width, y: 0), animated: true)
  271. }
  272. fileprivate func pageViewDidEndScroll() {
  273. guard self.scrollView.contentOffset.x != startOffset else { return }
  274. let index = Int(self.scrollView.contentOffset.x / contentRect.width)
  275. switch direction {
  276. case .left:
  277. didSelectMenuItem(index)
  278. case .right:
  279. didSelectMenuItem(index)
  280. }
  281. }
  282. }