FastScrollTableView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. //
  2. // FastScrollTableView.swift
  3. // FastScroll
  4. //
  5. // Created by Arsene Huot on 15/06/2018.
  6. // Copyright © 2018 Frichti. All rights reserved.
  7. //
  8. import Foundation
  9. import UIKit
  10. open class FastScrollTableView: UITableView {
  11. public enum BubbleFocus {
  12. case first
  13. case last
  14. case dynamic
  15. }
  16. // Bubble to display your information during scroll
  17. public var deactivateBubble: Bool = false
  18. public var bubble: UITextView?
  19. public var bubbleFont: UIFont = UIFont.systemFont(ofSize: 12.0)
  20. public var bubbleTextSize: CGFloat = 12.0
  21. public var bubbleTextColor: UIColor = UIColor.white
  22. public var bubbleRadius: CGFloat = 20.0
  23. public var bubblePadding: CGFloat = 12.0
  24. public var bubbleMarginRight: CGFloat = 30.0
  25. public var bubbleColor: UIColor = UIColor.darkGray
  26. public var bubbleShadowColor: UIColor = UIColor.darkGray
  27. public var bubbleShadowOpacity: Float = 0.7
  28. public var bubbleShadowRadius: CGFloat = 3.0
  29. public var bubbleShadowOffset: CGSize = CGSize(width: 0.0, height: 5.0)
  30. public var bubbleFocus: BubbleFocus = .first
  31. // Handler to scroll
  32. public var handle: UIView?
  33. public var handleImage: UIImage?
  34. public var handleWidth: CGFloat = 30.0
  35. public var handleHeight: CGFloat = 30.0
  36. public var handleRadius: CGFloat = 15.0
  37. public var handleMarginRight: CGFloat = 6.0
  38. public var handleShadowColor: UIColor = UIColor.darkGray
  39. public var handleShadowOpacity: Float = 0.7
  40. public var handleShadowOffset: CGSize = CGSize(width: 0.0, height: 5.0)
  41. public var handleShadowRadius: CGFloat = 3.0
  42. public var handleColor: UIColor = UIColor.darkGray
  43. public var handleTimeToDisappear: CGFloat = 1.5
  44. public var handleDisappearAnimationDuration: CGFloat = 0.2
  45. fileprivate var handleTouched: Bool = false
  46. // Gesture center on handler
  47. public var gestureHandleView: UIView?
  48. public var gestureWidth: CGFloat = 50.0
  49. public var gestureHeight: CGFloat = 50.0
  50. // Scrollbar
  51. public var scrollbar: UIView?
  52. public var scrollbarWidth: CGFloat = 2.0
  53. public var scrollbarColor: UIColor = UIColor(red: 220.0 / 255.0, green: 220.0 / 255.0, blue: 220.0 / 255.0, alpha: 1.0)
  54. public var scrollbarRadius: CGFloat = 1.0
  55. public var scrollbarMarginTop: CGFloat = 40.0
  56. public var scrollbarMarginBottom: CGFloat = 20.0
  57. public var scrollbarMarginRight: CGFloat = 20.0
  58. // Timer to dismiss handle
  59. fileprivate var handleTimer: Timer?
  60. // Action callback
  61. public var bubbleNameForIndexPath: (IndexPath) -> String = { _ in return ""}
  62. // MARK: LifeCycle
  63. override open func draw(_ rect: CGRect) {
  64. super.draw(rect)
  65. setup()
  66. setupCollectionView()
  67. }
  68. // MARK: Setups
  69. fileprivate func setupCollectionView() {
  70. showsVerticalScrollIndicator = false
  71. }
  72. public func setup() {
  73. cleanViews()
  74. setupScrollbar()
  75. setupHandle()
  76. setupBubble()
  77. }
  78. public func cleanViews() {
  79. guard let bubble = bubble, let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  80. return
  81. }
  82. bubble.removeFromSuperview()
  83. handle.removeFromSuperview()
  84. scrollbar.removeFromSuperview()
  85. gestureHandleView.removeFromSuperview()
  86. self.bubble = nil
  87. self.handle = nil
  88. self.scrollbar = nil
  89. self.gestureHandleView = nil
  90. }
  91. fileprivate func setupHandle() {
  92. if handle == nil {
  93. handle = UIView(frame: CGRect(x: self.frame.width - handleWidth - handleMarginRight, y: scrollbarMarginTop, width: handleWidth, height: handleHeight))
  94. self.superview?.addSubview(handle!)
  95. gestureHandleView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: gestureWidth, height: gestureHeight))
  96. gestureHandleView!.center = handle!.center
  97. self.superview?.addSubview(handle!)
  98. self.superview?.addSubview(gestureHandleView!)
  99. }
  100. //config layer
  101. handle!.backgroundColor = handleColor
  102. handle!.layer.cornerRadius = handleRadius
  103. handle!.layer.shadowColor = handleShadowColor.cgColor
  104. handle!.layer.shadowOffset = handleShadowOffset
  105. handle!.layer.shadowRadius = handleShadowRadius
  106. handle!.layer.shadowOpacity = handleShadowOpacity
  107. //set imageView
  108. if let handleImage = handleImage {
  109. let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: handleWidth, height: handleHeight))
  110. imageView.image = handleImage
  111. handle!.addSubview(imageView)
  112. }
  113. //set gesture
  114. let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
  115. gestureHandleView!.addGestureRecognizer(panGesture)
  116. //hide
  117. handle!.alpha = 0.0
  118. handle!.isHidden = true
  119. gestureHandleView!.isHidden = true
  120. //position
  121. positionHandle(scrollbarMarginTop)
  122. }
  123. fileprivate func setupBubble() {
  124. if bubble == nil {
  125. bubble = UITextView()
  126. self.superview?.addSubview(bubble!)
  127. }
  128. bubble!.font = bubbleFont
  129. bubble!.font = UIFont(name: bubbleFont.fontName, size: bubbleTextSize)
  130. bubble!.text = "Test"
  131. bubble!.textColor = bubbleTextColor
  132. bubble!.textAlignment = NSTextAlignment.center
  133. bubble!.textContainerInset = UIEdgeInsets(top: bubblePadding, left: bubblePadding, bottom: bubblePadding, right: bubblePadding)
  134. bubble!.contentMode = UIView.ContentMode.scaleAspectFit
  135. bubble!.sizeToFit()
  136. bubble!.backgroundColor = bubbleColor
  137. bubble!.layer.cornerRadius = bubbleRadius
  138. bubble!.layer.shadowColor = bubbleShadowColor.cgColor
  139. bubble!.layer.shadowOffset = bubbleShadowOffset
  140. bubble!.layer.shadowRadius = bubbleRadius
  141. bubble!.layer.shadowOpacity = bubbleShadowOpacity
  142. bubble!.layer.shadowRadius = bubbleShadowRadius
  143. bubble!.layer.masksToBounds = false
  144. bubble!.isHidden = true
  145. updateBubblePosition()
  146. }
  147. fileprivate func setupScrollbar() {
  148. guard let superview = self.superview else {
  149. return
  150. }
  151. if scrollbar == nil {
  152. scrollbar = UIView(frame: CGRect(x: self.frame.width - scrollbarWidth - scrollbarMarginRight, y: scrollbarMarginTop, width: scrollbarWidth, height: superview.bounds.height - scrollbarMarginBottom - scrollbarMarginTop))
  153. self.superview?.addSubview(scrollbar!)
  154. }
  155. scrollbar!.backgroundColor = scrollbarColor
  156. scrollbar!.layer.cornerRadius = scrollbarRadius
  157. scrollbar!.alpha = 0.0
  158. scrollbar!.isHidden = true
  159. }
  160. // MARK: Helpers
  161. @objc func hideHandle() {
  162. guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  163. return
  164. }
  165. gestureHandleView.isHidden = true
  166. UIView.animate(withDuration: TimeInterval(handleDisappearAnimationDuration), animations: {
  167. handle.alpha = 0.0
  168. scrollbar.alpha = 0.0
  169. }, completion: { finished in
  170. if finished {
  171. handle.isHidden = true
  172. scrollbar.isHidden = true
  173. }
  174. })
  175. }
  176. fileprivate func updateBubblePosition() {
  177. guard let scrollbar = scrollbar, let bubble = bubble, let handle = handle else {
  178. return
  179. }
  180. bubble.frame.origin.x = scrollbar.frame.origin.x - bubble.frame.size.width - bubbleMarginRight
  181. bubble.center.y = handle.center.y
  182. }
  183. fileprivate func positionHandle(_ y: CGFloat) {
  184. guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  185. return
  186. }
  187. handle.frame.origin.y = y >= scrollbarMarginTop ?
  188. (y > scrollbarMarginTop + scrollbar.frame.height - handle.frame.height) ? scrollbarMarginTop + scrollbar.frame.height - handle.frame.height : y
  189. :
  190. scrollbarMarginTop
  191. gestureHandleView.center = handle.center
  192. }
  193. fileprivate func scrollCollectionFromHandle() {
  194. guard let handle = handle, let scrollbar = scrollbar else {
  195. return
  196. }
  197. let collectionContentHeight = self.contentSize.height - self.bounds.height
  198. let scrollBarHeight = scrollbar.frame.height
  199. let scrollY = (handle.frame.origin.y - scrollbarMarginTop) * (collectionContentHeight / (scrollBarHeight - handle.frame.size.height))
  200. self.setContentOffset(CGPoint(x: 0.0, y: scrollY), animated: false)
  201. }
  202. @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
  203. guard let superview = superview, let bubble = bubble, let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  204. return
  205. }
  206. // get translation
  207. let translation = panGesture.translation(in: superview)
  208. panGesture.setTranslation(CGPoint.zero, in: superview)
  209. // manage start stop pan
  210. if panGesture.state == UIGestureRecognizer.State.began {
  211. bubble.isHidden = deactivateBubble ? true : false
  212. handleTouched = true
  213. //invalid hide timer
  214. if let handleTimer = handleTimer {
  215. handleTimer.invalidate()
  216. }
  217. handle.alpha = 1.0
  218. scrollbar.alpha = 1.0
  219. handle.isHidden = false
  220. scrollbar.isHidden = false
  221. gestureHandleView.isHidden = false
  222. }
  223. if panGesture.state == UIGestureRecognizer.State.ended {
  224. bubble.isHidden = true
  225. handleTouched = false
  226. self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
  227. }
  228. if panGesture.state == UIGestureRecognizer.State.changed {
  229. //invalid hide timer
  230. if let handleTimer = handleTimer {
  231. handleTimer.invalidate()
  232. }
  233. handle.alpha = 1.0
  234. scrollbar.alpha = 1.0
  235. handle.isHidden = false
  236. scrollbar.isHidden = false
  237. gestureHandleView.isHidden = false
  238. }
  239. // views positions
  240. positionHandle(handle.frame.origin.y + translation.y)
  241. updateBubblePosition()
  242. scrollCollectionFromHandle()
  243. // manage bubble info
  244. manageBubbleInfo()
  245. }
  246. fileprivate func manageBubbleInfo() {
  247. guard let bubble = bubble else {
  248. return
  249. }
  250. let visibleCells = self.visibleCells
  251. var currentCellIndex: Int
  252. switch bubbleFocus {
  253. case .first:
  254. currentCellIndex = 0
  255. case .last:
  256. currentCellIndex = visibleCells.count - 1
  257. case .dynamic:
  258. //Calcul scroll percentage
  259. let scrollY = contentOffset.y
  260. let collectionContentHeight = self.contentSize.height > self.bounds.height ? self.contentSize.height - self.bounds.height : self.bounds.height
  261. let scrollPercentage = scrollY / collectionContentHeight
  262. currentCellIndex = Int(floor(CGFloat(visibleCells.count) * scrollPercentage))
  263. }
  264. if currentCellIndex < visibleCells.count {
  265. if let indexPath = indexPath(for: visibleCells[currentCellIndex]) {
  266. bubble.text = bubbleNameForIndexPath(indexPath)
  267. let newSize = bubble.sizeThatFits(CGSize(width: self.bounds.width - (self.bounds.width - (bubble.frame.origin.x + bubble.frame.size.width)), height: bubble.frame.size.height))
  268. let oldSize = bubble.frame.size
  269. bubble.frame = CGRect(x: bubble.frame.origin.x + (oldSize.width - newSize.width), y: bubble.frame.origin.y, width: newSize.width, height: newSize.height)
  270. }
  271. }
  272. }
  273. }
  274. // MARK: Scroll Management
  275. extension FastScrollTableView {
  276. public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  277. guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  278. return
  279. }
  280. handle.alpha = 1.0
  281. scrollbar.alpha = 1.0
  282. handle.isHidden = false
  283. scrollbar.isHidden = false
  284. gestureHandleView.isHidden = false
  285. }
  286. public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  287. if !decelerate {
  288. self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
  289. }
  290. }
  291. public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  292. self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
  293. }
  294. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  295. guard let handle = handle, let scrollbar = scrollbar else {
  296. return
  297. }
  298. //invalid timer
  299. if let handleTimer = handleTimer {
  300. handleTimer.invalidate()
  301. }
  302. //scroll position
  303. let scrollY = scrollView.contentOffset.y
  304. let collectionContentHeight = self.contentSize.height > self.bounds.height ? self.contentSize.height - self.bounds.height : self.bounds.height
  305. let scrollBarHeight = scrollbar.frame.height
  306. let handlePosition = (scrollY / collectionContentHeight) * (scrollBarHeight - handle.frame.size.height) + scrollbarMarginTop
  307. if (handleTouched == false) {
  308. positionHandle(handlePosition)
  309. }
  310. updateBubblePosition()
  311. }
  312. }