No Description

PageViewController.swift 13KB

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