Advertisement
Guest User

Untitled

a guest
May 25th, 2019
58
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.19 KB | None | 0 0
  1. import UIKit
  2.  
  3. class PullUpController: BaseViewController {
  4.  
  5. private var leftConstraint: NSLayoutConstraint?
  6. private var topConstraint: NSLayoutConstraint?
  7. private var widthConstraint: NSLayoutConstraint?
  8. private var heightConstraint: NSLayoutConstraint?
  9. open var panGestureRecognizer: UIPanGestureRecognizer?
  10.  
  11. /**
  12. The desired height in screen units expressed in the pull up controller coordinate system that will be initially showed.
  13. The default value is 50.
  14. */
  15. open var pullUpControllerPreviewOffset: CGFloat {
  16. return 200
  17. }
  18.  
  19. /**
  20. The desired size of the pull up controller’s view, in screen units.
  21. The default value is width: UIScreen.main.bounds.width, height: 400.
  22. */
  23. open var pullUpControllerPreferredSize: CGSize {
  24. return CGSize(width: UIScreen.main.bounds.width * 0.95, height: UIScreen.main.bounds.height * 0.70)
  25. }
  26.  
  27. /**
  28. A list of y values, in screen units expressed in the pull up controller coordinate system.
  29. At the end of the gestures the pull up controller will scroll to the nearest point in the list.
  30.  
  31. Please keep in mind that this array should contains only sticky points in the middle of the pull up controller's view;
  32. There is therefore no need to add the fist one (pullUpControllerPreviewOffset), and/or the last one (pullUpControllerPreferredSize.height).
  33.  
  34. For a complete list of all the sticky points you can use `pullUpControllerAllStickyPoints`.
  35. */
  36. open var pullUpControllerMiddleStickyPoints: [CGFloat] {
  37. var result: [CGFloat] = []
  38. let numberOfMiddleStickPoints = 1
  39. let increment = (pullUpControllerPreferredSize.height - pullUpControllerPreviewOffset)/(CGFloat(numberOfMiddleStickPoints)+1)
  40. for i in 1...numberOfMiddleStickPoints {
  41. result.append(pullUpControllerPreviewOffset+CGFloat(i)*increment)
  42. }
  43. return result
  44. }
  45.  
  46. /**
  47. A list of y values, in screen units expressed in the pull up controller coordinate system.
  48. At the end of the gesture the pull up controller will scroll at the nearest point in the list.
  49. */
  50. public final var pullUpControllerAllStickyPoints: [CGFloat] {
  51. var sc_allStickyPoints = [pullUpControllerPreviewOffset - 115, pullUpControllerPreferredSize.height]
  52. sc_allStickyPoints.append(contentsOf: pullUpControllerMiddleStickyPoints)
  53. return sc_allStickyPoints.sorted()
  54. }
  55.  
  56. /**
  57. A Boolean value that determines whether bouncing occurs when scrolling reaches the end of the pull up controller's view size.
  58. The default value is false.
  59. */
  60. open var pullUpControllerIsBouncingEnabled: Bool {
  61. return true
  62. }
  63.  
  64. /**
  65. The desired size of the pull up controller’s view, in screen units when the device is in landscape mode.
  66. The default value is (x: 10, y: 10, width: 300, height: UIScreen.main.bounds.height - 20).
  67. */
  68. open var pullUpControllerPreferredLandscapeFrame: CGRect {
  69. return CGRect(x: 10, y: 10, width: 300, height: UIScreen.main.bounds.height - 20)
  70. }
  71.  
  72. private var isPortrait: Bool {
  73. return UIScreen.main.bounds.height > UIScreen.main.bounds.width
  74. }
  75.  
  76. private var portraitPreviousStickyPointIndex: Int?
  77.  
  78. /**
  79. This method will move the pull up controller's view in order to show the provided visible point.
  80.  
  81. You may use on of `pullUpControllerAllStickyPoints` item to provide a valid visible point.
  82. - parameter visiblePoint: the y value to make visible, in screen units expressed in the pull up controller coordinate system.
  83. - parameter completion: The closure to execute after the animation is completed. This block has no return value and takes no parameters. You may specify nil for this parameter.
  84. */
  85. open func pullUpControllerMoveToVisiblePoint(_ visiblePoint: CGFloat, completion: (() -> Void)?) {
  86. guard isPortrait else { return }
  87. topConstraint?.constant = (parent?.view.frame.height ?? 0) - visiblePoint
  88.  
  89. UIView.animate(withDuration: 0.3, animations: { [weak self] in
  90. self?.parent?.view?.layoutIfNeeded()
  91. }, completion: { _ in
  92. completion?()
  93. })
  94. }
  95.  
  96. open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  97. let isPortrait = size.height > size.width
  98. var targetStickyPoint: CGFloat?
  99.  
  100. if !isPortrait {
  101. portraitPreviousStickyPointIndex = currentStickyPointIndex
  102. } else if
  103. let portraitPreviousStickyPointIndex = portraitPreviousStickyPointIndex,
  104. portraitPreviousStickyPointIndex < pullUpControllerAllStickyPoints.count
  105. {
  106. targetStickyPoint = pullUpControllerAllStickyPoints[portraitPreviousStickyPointIndex]
  107. self.portraitPreviousStickyPointIndex = nil
  108. }
  109.  
  110. // swiftlint:disable:next unused_closure_parameter
  111. coordinator.animate(alongsideTransition: { [weak self] coordinator in
  112. self?.refreshConstraints(size: size)
  113. if let targetStickyPoint = targetStickyPoint {
  114. self?.pullUpControllerMoveToVisiblePoint(targetStickyPoint, completion: nil)
  115. }
  116. })
  117. }
  118.  
  119. fileprivate func setupPanGestureRecognizer() {
  120. panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGestureRecognizer(_:)))
  121. panGestureRecognizer?.minimumNumberOfTouches = 1
  122. panGestureRecognizer?.maximumNumberOfTouches = 1
  123. if let panGestureRecognizer = panGestureRecognizer {
  124. view.addGestureRecognizer(panGestureRecognizer)
  125. }
  126. }
  127.  
  128. fileprivate func setupConstrains() {
  129. guard let parentView = parent?.view else { return }
  130.  
  131. topConstraint = view.topAnchor.constraint(equalTo: parentView.topAnchor, constant: 0)
  132. leftConstraint = view.leftAnchor.constraint(equalTo: parentView.leftAnchor, constant: 0)
  133. widthConstraint = view.widthAnchor.constraint(equalToConstant: pullUpControllerPreferredSize.width)
  134. heightConstraint = view.heightAnchor.constraint(equalToConstant: pullUpControllerPreferredSize.height)
  135.  
  136. NSLayoutConstraint.activate([topConstraint, leftConstraint, widthConstraint, heightConstraint].compactMap { $0 })
  137. }
  138.  
  139. open var currentStickyPointIndex: Int {
  140. let stickyPointTreshold = (self.parent?.view.frame.height ?? 0) - (topConstraint?.constant ?? 0)
  141. let stickyPointsLessCurrentPosition = pullUpControllerAllStickyPoints.map { abs($0 - stickyPointTreshold) }
  142. guard let minStickyPointDifference = stickyPointsLessCurrentPosition.min() else { return 0 }
  143. return stickyPointsLessCurrentPosition.index(of: minStickyPointDifference) ?? 0
  144. }
  145.  
  146. private func nearestStickyPointY(yVelocity: CGFloat) -> CGFloat {
  147. var currentStickyPointIndex = self.currentStickyPointIndex
  148. if abs(yVelocity) > 700 { // 1000 points/sec = "fast" scroll
  149. if yVelocity > 0 {
  150. currentStickyPointIndex = max(currentStickyPointIndex - 1, 0)
  151. } else {
  152. currentStickyPointIndex = min(currentStickyPointIndex + 1, pullUpControllerAllStickyPoints.count - 1)
  153. }
  154. }
  155. return (parent?.view.frame.height ?? 0) - pullUpControllerAllStickyPoints[currentStickyPointIndex]
  156. }
  157.  
  158. @objc open func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
  159. guard
  160. isPortrait,
  161. let topConstraint = topConstraint,
  162. let parentViewHeight = parent?.view.frame.height
  163. else { return }
  164.  
  165. let yTranslation = gestureRecognizer.translation(in: view).y
  166. gestureRecognizer.setTranslation(.zero, in: view)
  167.  
  168. topConstraint.constant += yTranslation
  169.  
  170. if !pullUpControllerIsBouncingEnabled {
  171. topConstraint.constant = max(topConstraint.constant, parentViewHeight - pullUpControllerPreferredSize.height)
  172. topConstraint.constant = min(topConstraint.constant, parentViewHeight - pullUpControllerPreviewOffset)
  173. }
  174.  
  175. if gestureRecognizer.state == .ended {
  176. topConstraint.constant = nearestStickyPointY(yVelocity: gestureRecognizer.velocity(in: view).y)
  177. UIView.animate(
  178. withDuration: 0.3,
  179. animations: { [weak self] in
  180. self?.parent?.view.layoutIfNeeded()
  181. }
  182. )
  183. }
  184. }
  185.  
  186. @objc fileprivate func handleInternalScrollViewPanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
  187. guard
  188. isPortrait,
  189. let scrollView = gestureRecognizer.view as? UIScrollView,
  190. let lastStickyPoint = pullUpControllerAllStickyPoints.last,
  191. let parentViewHeight = parent?.view.frame.height,
  192. let topConstraintValue = topConstraint?.constant
  193. else { return }
  194.  
  195. let isScrollingDown = gestureRecognizer.translation(in: view).y > 0
  196. let shouldScrollingDownTriggerGestureRecognizer = isScrollingDown && scrollView.contentOffset.y <= 0
  197. let shouldScrollingUpTriggerGestureRecognizer = !isScrollingDown && topConstraintValue != parentViewHeight - lastStickyPoint
  198.  
  199. if shouldScrollingDownTriggerGestureRecognizer || shouldScrollingUpTriggerGestureRecognizer {
  200. handlePanGestureRecognizer(gestureRecognizer)
  201. }
  202.  
  203. if gestureRecognizer.state.rawValue == 3 { // for some reason gestureRecognizer.state == .ended doesn't work
  204. topConstraint?.constant = nearestStickyPointY(yVelocity: 0)
  205. UIView.animate(
  206. withDuration: 0.3,
  207. animations: { [weak self] in
  208. self?.parent?.view.layoutIfNeeded()
  209. }
  210. )
  211. }
  212. }
  213.  
  214. private func setPortraitConstraints(parentViewSize: CGSize) {
  215. topConstraint?.constant = parentViewSize.height - pullUpControllerPreviewOffset
  216. leftConstraint?.constant = (parentViewSize.width - min(pullUpControllerPreferredSize.width, parentViewSize.width))/2
  217. widthConstraint?.constant = pullUpControllerPreferredSize.width
  218. heightConstraint?.constant = pullUpControllerPreferredSize.height
  219. }
  220.  
  221. private func setLandscapeConstraints() {
  222. topConstraint?.constant = pullUpControllerPreferredLandscapeFrame.origin.y
  223. leftConstraint?.constant = pullUpControllerPreferredLandscapeFrame.origin.x
  224. widthConstraint?.constant = pullUpControllerPreferredLandscapeFrame.width
  225. heightConstraint?.constant = pullUpControllerPreferredLandscapeFrame.height
  226. }
  227.  
  228. fileprivate func refreshConstraints(size: CGSize) {
  229. if size.width > size.height {
  230. setLandscapeConstraints()
  231. } else {
  232. setPortraitConstraints(parentViewSize: size)
  233. }
  234. }
  235. }
  236.  
  237. extension UIViewController {
  238.  
  239. /**
  240. Adds the specified pull up view controller as a child of the current view controller.
  241. - parameter pullUpController: the pull up controller to add as a child of the current view controller.
  242. */
  243. func addPullUpController(_ pullUpController: PullUpController) {
  244. addChildViewController(pullUpController)
  245.  
  246. pullUpController.view.translatesAutoresizingMaskIntoConstraints = false
  247. view.addSubview(pullUpController.view)
  248.  
  249. pullUpController.setupPanGestureRecognizer()
  250. pullUpController.setupConstrains()
  251. pullUpController.refreshConstraints(size: view.frame.size)
  252. }
  253. }
  254.  
  255. extension UIScrollView {
  256.  
  257. /**
  258. Attach the scroll view to the provided pull up controller in order to move it with the scroll view content.
  259. - parameter pullUpController: the pull up controller to move with the current scroll view content.
  260. */
  261. func attach(to pullUpController: PullUpController) {
  262. panGestureRecognizer.addTarget(pullUpController, action: #selector(pullUpController.handleInternalScrollViewPanGestureRecognizer(_:)))
  263. }
  264. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement