MultiSlider+Internal.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. //
  2. // MultiSlider+Internal.swift
  3. // MultiSlider
  4. //
  5. // Created by Yonat Sharon on 21/06/2019.
  6. //
  7. import UIKit
  8. extension MultiSlider {
  9. func setup() {
  10. trackView.backgroundColor = actualTintColor
  11. updateTrackViewCornerRounding()
  12. slideView.layoutMargins = .zero
  13. setupOrientation()
  14. setupPanGesture()
  15. isAccessibilityElement = true
  16. accessibilityIdentifier = "multi_slider"
  17. accessibilityLabel = "slider"
  18. accessibilityTraits = [.allowsDirectInteraction]
  19. minimumView.isHidden = true
  20. maximumView.isHidden = true
  21. if #available(iOS 11.0, *) {
  22. valueLabelFormatter.addObserverForAllProperties(observer: self)
  23. }
  24. selectionFeedbackGenerator = UISelectionFeedbackGenerator()
  25. }
  26. private func setupPanGesture() {
  27. addConstrainedSubview(panGestureView)
  28. for edge: NSLayoutConstraint.Attribute in [.top, .bottom, .left, .right] {
  29. constrain(panGestureView, at: edge, diff: -edge.inwardSign * margin)
  30. }
  31. let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didDrag(_:)))
  32. panGesture.delegate = self
  33. panGestureView.addGestureRecognizer(panGesture)
  34. }
  35. func setupOrientation() {
  36. trackView.removeFromSuperview()
  37. trackView.removeConstraints(trackView.constraints)
  38. slideView.removeFromSuperview()
  39. minimumView.removeFromSuperview()
  40. maximumView.removeFromSuperview()
  41. switch orientation {
  42. case .vertical:
  43. let centerAttribute: NSLayoutConstraint.Attribute
  44. if #available(iOS 12, *) {
  45. centerAttribute = .centerX // iOS 12 doesn't like .topMargin, .rightMargin
  46. } else {
  47. centerAttribute = .centerXWithinMargins
  48. }
  49. addConstrainedSubview(trackView, constrain: .top, .bottom, centerAttribute)
  50. trackView.constrain(.width, to: trackWidth)
  51. trackView.addConstrainedSubview(slideView, constrain: .left, .right)
  52. constrainVerticalTrackViewToLayoutMargins()
  53. addConstrainedSubview(minimumView, constrain: .bottomMargin, centerAttribute)
  54. addConstrainedSubview(maximumView, constrain: .topMargin, centerAttribute)
  55. default:
  56. let centerAttribute: NSLayoutConstraint.Attribute
  57. if #available(iOS 12, *) {
  58. centerAttribute = .centerY // iOS 12 doesn't like .leftMargin, .rightMargin
  59. } else {
  60. centerAttribute = .centerYWithinMargins
  61. }
  62. addConstrainedSubview(trackView, constrain: .left, .right, centerAttribute)
  63. trackView.constrain(.height, to: trackWidth)
  64. trackView.addConstrainedSubview(slideView, constrain: .top, .bottom)
  65. constrainHorizontalTrackViewToLayoutMargins()
  66. addConstrainedSubview(minimumView, constrain: .leftMargin, centerAttribute)
  67. addConstrainedSubview(maximumView, constrain: .rightMargin, centerAttribute)
  68. }
  69. setupTrackLayoutMargins()
  70. }
  71. func setupTrackLayoutMargins() {
  72. let thumbSize = (thumbImage ?? defaultThumbImage)?.size ?? CGSize(width: 2, height: 2)
  73. let thumbDiameter = orientation == .vertical ? thumbSize.height : thumbSize.width
  74. let margin = (centerThumbOnTrackEnd || nil != snapImage)
  75. ? 0
  76. : thumbDiameter / 2 - 1 // 1 pixel for semi-transparent boundary
  77. if orientation == .vertical {
  78. trackView.layoutMargins = UIEdgeInsets(top: margin, left: 0, bottom: margin, right: 0)
  79. constrainVerticalTrackViewToLayoutMargins()
  80. constrain(.width, to: max(thumbSize.width, trackWidth), relation: .greaterThanOrEqual)
  81. } else {
  82. trackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
  83. constrainHorizontalTrackViewToLayoutMargins()
  84. constrain(.height, to: max(thumbSize.height, trackWidth), relation: .greaterThanOrEqual)
  85. }
  86. }
  87. /// workaround to a problem in iOS 12-13, of constraining to `leftMargin` and `rightMargin`.
  88. func constrainHorizontalTrackViewToLayoutMargins() {
  89. trackView.constrain(slideView, at: .left, diff: trackView.layoutMargins.left)
  90. trackView.constrain(slideView, at: .right, diff: -trackView.layoutMargins.right)
  91. }
  92. /// workaround to a problem in iOS 12-13, of constraining to `topMargin` and `bottomMargin`.
  93. func constrainVerticalTrackViewToLayoutMargins() {
  94. trackView.constrain(slideView, at: .top, diff: trackView.layoutMargins.top)
  95. trackView.constrain(slideView, at: .bottom, diff: -trackView.layoutMargins.bottom)
  96. }
  97. func repositionThumbViews() {
  98. thumbViews.forEach { $0.removeFromSuperview() }
  99. thumbViews = []
  100. valueLabels.forEach { $0.removeFromSuperview() }
  101. valueLabels = []
  102. adjustThumbCountToValueCount()
  103. }
  104. func adjustThumbCountToValueCount() {
  105. guard value.count != thumbViews.count else { return }
  106. thumbViews.removeAllViews()
  107. valueLabels.removeAllViews()
  108. for _ in value {
  109. addThumbView()
  110. }
  111. updateOuterTrackViews()
  112. }
  113. func updateOuterTrackViews() {
  114. outerTrackViews.removeAllViews()
  115. outerTrackViews.removeAll()
  116. guard nil != outerTrackColor else { return }
  117. guard let lastThumb = thumbViews.last else { return }
  118. outerTrackViews = [outerTrackView(constraining: .bottom(in: orientation), to: lastThumb)]
  119. guard let firstThumb = thumbViews.first, firstThumb != lastThumb else { return }
  120. outerTrackViews += [outerTrackView(constraining: .top(in: orientation), to: firstThumb)]
  121. }
  122. private func outerTrackView(constraining: NSLayoutConstraint.Attribute, to thumbView: UIView) -> UIView {
  123. let view = UIView()
  124. view.backgroundColor = outerTrackColor
  125. trackView.addConstrainedSubview(view, constrain: .top, .bottom, .left, .right)
  126. trackView.removeFirstConstraint { $0.firstItem === view && $0.firstAttribute == constraining }
  127. trackView.constrain(view, at: constraining, to: thumbView, at: .center(in: orientation))
  128. trackView.sendSubviewToBack(view)
  129. view.layer.cornerRadius = trackView.layer.cornerRadius
  130. if #available(iOS 11.0, *) {
  131. view.layer.maskedCorners = .direction(constraining.opposite)
  132. }
  133. return view
  134. }
  135. func addSnapView(at snapValue: CGFloat) {
  136. let snapView = UIImageView(image: snapImage)
  137. snapView.tintColor = actualTintColor
  138. snapViews.append(snapView)
  139. slideView.addConstrainedSubview(snapView, constrain: NSLayoutConstraint.Attribute.center(in: orientation).perpendicularCenter)
  140. slideView.sendSubviewToBack(snapView)
  141. position(marker: snapView, at: snapValue)
  142. }
  143. private func addThumbView() {
  144. let i = thumbViews.count
  145. let thumbView = UIImageView(image: thumbImage ?? defaultThumbImage)
  146. thumbView.applyTint(color: thumbTintColor)
  147. thumbView.addShadow()
  148. thumbViews.append(thumbView)
  149. slideView.addConstrainedSubview(thumbView, constrain: NSLayoutConstraint.Attribute.center(in: orientation).perpendicularCenter)
  150. positionThumbView(i)
  151. thumbView.blur(disabledThumbIndices.contains(i))
  152. addValueLabel(i)
  153. updateThumbViewShadowVisibility()
  154. }
  155. func updateThumbViewShadowVisibility() {
  156. thumbViews.forEach {
  157. $0.layer.shadowOpacity = showsThumbImageShadow ? 0.25 : 0
  158. }
  159. }
  160. func addValueLabel(_ i: Int) {
  161. guard valueLabelPosition != .notAnAttribute else { return }
  162. let valueLabel = UITextField()
  163. valueLabel.borderStyle = .none
  164. slideView.addConstrainedSubview(valueLabel)
  165. valueLabel.textColor = valueLabelColor ?? valueLabel.textColor
  166. valueLabel.font = valueLabelFont ?? UIFont.preferredFont(forTextStyle: .footnote)
  167. if #available(iOS 10.0, *) {
  168. valueLabel.adjustsFontForContentSizeCategory = true
  169. }
  170. let thumbView = thumbViews[i]
  171. slideView.constrain(valueLabel, at: valueLabelPosition.perpendicularCenter, to: thumbView)
  172. let position = valueLabelAlternatePosition && (i % 2) == 0
  173. ? valueLabelPosition.opposite
  174. : valueLabelPosition
  175. slideView.constrain(
  176. valueLabel, at: position.opposite,
  177. to: thumbView, at: position,
  178. diff: -position.inwardSign * thumbView.diagonalSize / 4
  179. )
  180. valueLabels.append(valueLabel)
  181. updateValueLabel(i)
  182. }
  183. func updateValueLabel(_ i: Int) {
  184. let labelValue: CGFloat
  185. if isValueLabelRelative {
  186. labelValue = i > 0 ? value[i] - value[i - 1] : value[i] - minimumValue
  187. } else {
  188. labelValue = value[i]
  189. }
  190. valueLabels[i].text = valueLabelText(i, labelValue: labelValue)
  191. }
  192. func valueLabelText(_ i: Int, labelValue: CGFloat) -> String? {
  193. valueLabelTextForThumb?(i, labelValue)
  194. ?? valueLabelFormatter.string(from: NSNumber(value: Double(labelValue)))
  195. }
  196. func updateAllValueLabels() {
  197. for i in 0 ..< valueLabels.count {
  198. updateValueLabel(i)
  199. }
  200. }
  201. func updateValueLabelPosition() {
  202. valueLabels.removeAllViews()
  203. if valueLabelPosition != .notAnAttribute {
  204. for i in 0 ..< thumbViews.count {
  205. addValueLabel(i)
  206. }
  207. }
  208. }
  209. func updateValueCount(_ count: Int) {
  210. guard count != value.count else { return }
  211. isSettingValue = true
  212. defer { isSettingValue = false }
  213. if value.count < count {
  214. let appendCount = count - value.count
  215. value += snapValues.isEmpty
  216. ? value.distributedNewValues(count: appendCount, min: minimumValue, max: maximumValue)
  217. : value.distributedNewValues(count: appendCount, allowedValues: snapValues)
  218. value.sort()
  219. }
  220. if value.count > count { // don't add "else", since prev calc may add too many values in some cases
  221. value.removeLast(value.count - count)
  222. }
  223. }
  224. func adjustValuesToStepAndLimits() {
  225. var adjusted = value.sorted()
  226. for i in 0 ..< adjusted.count {
  227. adjusted[i] = snap.snap(value: adjusted[i])
  228. }
  229. isSettingValue = true
  230. value = adjusted
  231. isSettingValue = false
  232. for i in 0 ..< value.count {
  233. positionThumbView(i)
  234. }
  235. }
  236. func positionThumbView(_ i: Int) {
  237. position(marker: thumbViews[i], at: value[i])
  238. }
  239. private func position(marker: UIView, at value: CGFloat) {
  240. guard let containerView = marker.superview else { return }
  241. containerView.removeFirstConstraint { $0.firstItem === marker && $0.firstAttribute == .center(in: orientation) }
  242. let minMaxValueDifference = maximumValue - minimumValue
  243. let relativeDistanceToMax = minMaxValueDifference.isZero ? 0 : (maximumValue - value) / minMaxValueDifference
  244. if orientation == .horizontal {
  245. if relativeDistanceToMax < 1 {
  246. containerView.constrain(marker, at: .centerX, to: containerView, at: .right, ratio: CGFloat(1 - relativeDistanceToMax))
  247. } else {
  248. containerView.constrain(marker, at: .centerX, to: containerView, at: .left)
  249. }
  250. } else { // vertical orientation
  251. if relativeDistanceToMax.isNormal {
  252. containerView.constrain(marker, at: .centerY, to: containerView, at: .bottom, ratio: CGFloat(relativeDistanceToMax))
  253. } else {
  254. containerView.constrain(marker, at: .centerY, to: containerView, at: .top)
  255. }
  256. }
  257. UIView.animate(withDuration: 0.1) {
  258. containerView.updateConstraintsIfNeeded()
  259. }
  260. }
  261. func changePositionConstraint(for subview: UIView?, to constant: CGFloat) {
  262. guard let constraint = subview?.superview?.constraints.first(where: { $0.firstItem === subview && $0.firstAttribute == .center(in: orientation) }) else { return }
  263. constraint.constant = constant * constraint.secondAttribute.inwardSign
  264. }
  265. func layoutTrackEdge(toView: UIImageView, edge: NSLayoutConstraint.Attribute, superviewEdge: NSLayoutConstraint.Attribute) {
  266. removeFirstConstraint { $0.firstItem === self.trackView && ($0.firstAttribute == edge || $0.firstAttribute == superviewEdge) }
  267. if nil != toView.image {
  268. constrain(trackView, at: edge, to: toView, at: edge.opposite, diff: edge.inwardSign * 8)
  269. } else {
  270. constrain(trackView, at: edge, to: self, at: superviewEdge)
  271. }
  272. }
  273. func updateTrackViewCornerRounding() {
  274. trackView.layer.cornerRadius = hasRoundTrackEnds ? trackWidth / 2 : 1
  275. outerTrackViews.forEach { $0.layer.cornerRadius = trackView.layer.cornerRadius }
  276. }
  277. }