MultiSlider+Drag.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. //
  2. // MultiSlider+Drag.swift
  3. // MultiSlider
  4. //
  5. // Created by Yonat Sharon on 25.10.2018.
  6. //
  7. import UIKit
  8. extension MultiSlider: UIGestureRecognizerDelegate {
  9. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
  10. guard let panGesture = otherGestureRecognizer as? UIPanGestureRecognizer else { return false }
  11. let velocity = panGesture.velocity(in: self)
  12. let panOrientation: NSLayoutConstraint.Axis = abs(velocity.y) > abs(velocity.x) ? .vertical : .horizontal
  13. return panOrientation != orientation
  14. }
  15. @objc open func didDrag(_ panGesture: UIPanGestureRecognizer) {
  16. switch panGesture.state {
  17. case .began:
  18. // determine thumb to drag
  19. let location = panGesture.location(in: slideView)
  20. draggedThumbIndex = closestThumb(point: location)
  21. case .ended, .cancelled, .failed:
  22. sendActions(for: .touchUpInside) // no bounds check for now (.touchUpInside vs .touchUpOutside)
  23. if !isContinuous { sendActions(for: [.valueChanged, .primaryActionTriggered]) }
  24. default:
  25. break
  26. }
  27. guard draggedThumbIndex >= 0 else { return }
  28. let slideViewLength = slideView.bounds.size(in: orientation)
  29. var targetPosition = panGesture.location(in: slideView).coordinate(in: orientation)
  30. // don't cross prev/next thumb and total range
  31. targetPosition = boundedDraggedThumbPosition(targetPosition: targetPosition)
  32. // change corresponding value
  33. updateDraggedThumbValue(relativeValue: targetPosition / slideViewLength)
  34. UIView.animate(withDuration: 0.1) {
  35. self.updateDraggedThumbPositionAndLabel()
  36. self.layoutIfNeeded()
  37. }
  38. }
  39. /// adjusted position that doesn't cross prev/next thumb and total range
  40. private func boundedDraggedThumbPosition(targetPosition: CGFloat) -> CGFloat {
  41. var delta: CGFloat = 0 // distance between thumbs in view coordinates
  42. if distanceBetweenThumbs < 0 {
  43. delta = thumbViews[draggedThumbIndex].frame.size(in: orientation) / 2
  44. } else if distanceBetweenThumbs > 0 && distanceBetweenThumbs < maximumValue - minimumValue {
  45. delta = (distanceBetweenThumbs / (maximumValue - minimumValue)) * slideView.bounds.size(in: orientation)
  46. }
  47. if orientation == .horizontal { delta = -delta }
  48. let bottomLimit = draggedThumbIndex > 0
  49. ? thumbViews[draggedThumbIndex - 1].center.coordinate(in: orientation) - delta
  50. : slideView.bounds.bottom(in: orientation)
  51. let topLimit = draggedThumbIndex < thumbViews.count - 1
  52. ? thumbViews[draggedThumbIndex + 1].center.coordinate(in: orientation) + delta
  53. : slideView.bounds.top(in: orientation)
  54. if orientation == .vertical {
  55. return min(bottomLimit, max(targetPosition, topLimit))
  56. } else {
  57. return max(bottomLimit, min(targetPosition, topLimit))
  58. }
  59. }
  60. private func updateDraggedThumbValue(relativeValue: CGFloat) {
  61. var newValue = relativeValue * (maximumValue - minimumValue)
  62. if orientation == .vertical {
  63. newValue = maximumValue - newValue
  64. } else {
  65. newValue += minimumValue
  66. }
  67. newValue = snap.snap(value: newValue)
  68. guard newValue != value[draggedThumbIndex] else { return }
  69. isSettingValue = true
  70. value[draggedThumbIndex] = newValue
  71. isSettingValue = false
  72. if snap != .never || relativeValue == 0 || relativeValue == 1 {
  73. selectionFeedbackGenerator?.selectionChanged()
  74. }
  75. if isContinuous { sendActions(for: [.valueChanged, .primaryActionTriggered]) }
  76. }
  77. private func updateDraggedThumbPositionAndLabel() {
  78. positionThumbView(draggedThumbIndex)
  79. if draggedThumbIndex < valueLabels.count {
  80. updateValueLabel(draggedThumbIndex)
  81. if isValueLabelRelative && draggedThumbIndex + 1 < valueLabels.count {
  82. updateValueLabel(draggedThumbIndex + 1)
  83. }
  84. }
  85. UIAccessibility.post(notification: .announcement, argument: valueLabelText(draggedThumbIndex, labelValue: value[draggedThumbIndex]))
  86. }
  87. private func closestThumb(point: CGPoint) -> Int {
  88. var closest = -1
  89. var minimumDistance = CGFloat.greatestFiniteMagnitude
  90. let pointCoordinate = point.coordinate(in: orientation)
  91. for i in 0 ..< thumbViews.count {
  92. guard !disabledThumbIndices.contains(i) else { continue }
  93. let thumbCoordinate = thumbViews[i].center.coordinate(in: orientation)
  94. let distance = abs(pointCoordinate - thumbCoordinate)
  95. if distance > minimumDistance { break }
  96. if i > 0 && closest == i - 1 && thumbViews[i].center == thumbViews[i - 1].center { // overlapping thumbs
  97. let greaterSign: CGFloat = orientation == .vertical ? -1 : 1
  98. if greaterSign * thumbCoordinate < greaterSign * pointCoordinate {
  99. closest = i
  100. }
  101. break
  102. }
  103. minimumDistance = distance
  104. if distance < thumbViews[i].diagonalSize + thumbTouchExpansionRadius {
  105. closest = i
  106. }
  107. }
  108. return closest
  109. }
  110. }