@@ -16,12 +16,20 @@ public protocol MSCircularSliderDelegate: MSCircularSliderProtocol {
1616 func circularSlider( _ slider: MSCircularSlider , valueChangedTo value: Double , fromUser: Bool ) // fromUser indicates whether the value changed by sliding the handle (fromUser == true) or through other means (fromUser == false)
1717 func circularSlider( _ slider: MSCircularSlider , startedTrackingWith value: Double )
1818 func circularSlider( _ slider: MSCircularSlider , endedTrackingWith value: Double )
19+ func circularSlider( _ slider: MSCircularSlider , directionChangedTo value: MSCircularSliderDirection )
20+ func circularSlider( _ slider: MSCircularSlider , revolutionsChangedTo value: Int )
1921}
2022
2123extension MSCircularSliderDelegate {
2224 // Optional Methods
2325 func circularSlider( _ slider: MSCircularSlider , startedTrackingWith value: Double ) { }
2426 func circularSlider( _ slider: MSCircularSlider , endedTrackingWith value: Double ) { }
27+ func circularSlider( _ slider: MSCircularSlider , directionChangedTo value: MSCircularSliderDirection ) { }
28+ func circularSlider( _ slider: MSCircularSlider , revolutionsChangedTo value: Int ) { }
29+ }
30+
31+ public enum MSCircularSliderDirection {
32+ case clockwise, counterclockwise, none
2533}
2634
2735
@@ -115,6 +123,30 @@ public class MSCircularSlider: UIControl {
115123 }
116124 }
117125
126+ /** Uniform padding */
127+ public var sliderPadding : CGFloat = 0.0 {
128+ didSet {
129+ setNeedsDisplay ( )
130+ }
131+ }
132+
133+ // REVOLUTIONS AND DIRECTION MEMBERS
134+
135+ /** Specifies the current handle sliding direction - *default: .none* */
136+ public var slidingDirection : MSCircularSliderDirection = . none
137+
138+ /** Indicates whether the user is sliding the handle or not - *default: false* */
139+ internal var isSliding : Bool = false
140+
141+ /** Counts how many revolutions have been made (works only when `maximumAngle` = 360.0 and `boundedMaxAngle` = false) - *default: 0* */
142+ private var revolutionsCount : Int = 0
143+
144+ /** Sets the maximum number of revolutions before the slider gets bounded at angle 360.0 (setting a -ve value will let the slider endlessly revolve; valid only for fully circular sliders) - *default: -1* */
145+ public var maximumRevolutions : Int = - 1
146+
147+ /** A constant threshold used to detect how many revolutions have passed - *default: 180.0* */
148+ internal let REVOLUTIONS_DETECTION_THRESHOLD : CGFloat = 180.0
149+
118150 // LINE MEMBERS
119151
120152 /** The slider's line width - *default: 5* */
@@ -327,12 +359,11 @@ public class MSCircularSlider: UIControl {
327359
328360 /** The slider's calculated radius based on the components' sizes */
329361 public var calculatedRadius : CGFloat {
330- if ( radius == - 1.0 ) {
331- let minimumSize = min ( bounds. size. height, bounds. size. width)
332- let halfLineWidth = ceilf ( Float ( lineWidth) / 2.0 )
333- let halfHandleWidth = ceilf ( Float ( handleDiameter) / 2.0 )
334- return minimumSize * 0.5 - CGFloat( max ( halfHandleWidth, halfLineWidth) )
335- }
362+ let minimumSize = min ( bounds. size. height - sliderPadding, bounds. size. width - sliderPadding)
363+ let halfLineWidth = ceilf ( Float ( lineWidth) / 2.0 )
364+ let halfHandleWidth = ceilf ( Float ( handleDiameter) / 2.0 )
365+ radius = minimumSize * 0.5 - CGFloat( max ( halfHandleWidth, halfLineWidth) )
366+
336367 return radius
337368 }
338369
@@ -346,6 +377,12 @@ public class MSCircularSlider: UIControl {
346377 return maximumAngle == 360.0
347378 }
348379
380+ /** A read-only property that indicates whether the slider endlessly loops or is bounded by a number of revolutions */
381+ public var endlesslyLoops : Bool {
382+ return maximumRevolutions == - 1
383+ }
384+
385+
349386 //================================================================================
350387 // SETTER METHODS
351388 //================================================================================
@@ -356,6 +393,7 @@ public class MSCircularSlider: UIControl {
356393
357394 setNeedsUpdateConstraints ( )
358395 setNeedsDisplay ( )
396+ setNeedsLayout ( )
359397 }
360398
361399 /** Replaces the label at a certain index with the given string */
@@ -365,6 +403,7 @@ public class MSCircularSlider: UIControl {
365403
366404 setNeedsUpdateConstraints ( )
367405 setNeedsDisplay ( )
406+ setNeedsLayout ( )
368407 }
369408
370409 /** Removes a label at a given index */
@@ -374,6 +413,7 @@ public class MSCircularSlider: UIControl {
374413
375414 setNeedsUpdateConstraints ( )
376415 setNeedsDisplay ( )
416+ setNeedsLayout ( )
377417 }
378418
379419 //================================================================================
@@ -391,7 +431,6 @@ public class MSCircularSlider: UIControl {
391431 super. init ( frame: frame)
392432 backgroundColor = . clear
393433 initHandle ( )
394-
395434 }
396435
397436 required public init ? ( coder aDecoder: NSCoder ) {
@@ -401,12 +440,8 @@ public class MSCircularSlider: UIControl {
401440 }
402441
403442 override public var intrinsicContentSize : CGSize {
404- let diameter = radius * 2
405- let handleRadius = ceilf ( Float ( handleDiameter) / 2.0 )
406-
407- let totalWidth = diameter + CGFloat( 2 * max( handleRadius, ceilf ( Float ( lineWidth) / 2.0 ) ) )
408-
409- return CGSize ( width: totalWidth, height: totalWidth)
443+ let diameter = calculatedRadius * 2
444+ return CGSize ( width: diameter, height: diameter)
410445 }
411446
412447 override public func draw( _ rect: CGRect ) {
@@ -431,7 +466,6 @@ public class MSCircularSlider: UIControl {
431466 for view in subviews { // cancel rotation on all subviews added by the user
432467 view. transform = rotationalTransform. inverted ( )
433468 }
434-
435469 }
436470
437471 override public func point( inside point: CGPoint , with event: UIEvent ? ) -> Bool {
@@ -440,7 +474,6 @@ public class MSCircularSlider: UIControl {
440474 }
441475
442476 if handle. contains ( point) {
443-
444477 return true
445478 }
446479 else {
@@ -462,10 +495,65 @@ public class MSCircularSlider: UIControl {
462495 }
463496
464497 override public func continueTracking( _ touch: UITouch , with event: UIEvent ? ) -> Bool {
465- let lastPoint = touch. location ( in: self )
466- let lastAngle = floor ( calculateAngle ( from: centerPoint, to: lastPoint) )
498+ let newPoint = touch. location ( in: self )
499+ var newAngle = floor ( calculateAngle ( from: centerPoint, to: newPoint) )
500+
501+ // Sliding direction and revolutions' count detection
502+ func changeSlidingDirection( _ direction: MSCircularSliderDirection ) {
503+ // Change direction (without multiplicity)
504+ if self . slidingDirection != direction {
505+ self . slidingDirection = direction
506+ self . castDelegate? . circularSlider ( self , directionChangedTo: slidingDirection)
507+ }
508+ }
467509
468- moveHandle ( newAngle: lastAngle)
510+ if newAngle > angle {
511+ // Check if crossing the critical point (north/angle=0.0)
512+ if fullCircle && isSliding && maximumRevolutions != - 1 && ( newAngle - angle > REVOLUTIONS_DETECTION_THRESHOLD) {
513+ // Check if should be bound (maximumRevolutions reached or less than 0)
514+ if revolutionsCount - 1 < 0 {
515+ newAngle = 0.0
516+ }
517+ else {
518+ revolutionsCount -= 1
519+ castDelegate? . circularSlider ( self , revolutionsChangedTo: revolutionsCount)
520+ changeSlidingDirection ( . counterclockwise)
521+ }
522+ }
523+ else if fullCircle && isSliding && ( newAngle - angle > REVOLUTIONS_DETECTION_THRESHOLD) {
524+ changeSlidingDirection ( . counterclockwise)
525+ }
526+ else {
527+ changeSlidingDirection ( . clockwise)
528+ }
529+
530+ }
531+ else if newAngle < angle {
532+ // Check if crossing the critical point (north/angle=0.0)
533+ if fullCircle && isSliding && maximumRevolutions != - 1 && ( angle - newAngle > REVOLUTIONS_DETECTION_THRESHOLD) {
534+ // Check if should be bound (maximumRevolutions reached or less than 0)
535+ if revolutionsCount + 1 > maximumRevolutions {
536+ newAngle = 360.0
537+ }
538+ else {
539+ revolutionsCount += 1
540+ castDelegate? . circularSlider ( self , revolutionsChangedTo: revolutionsCount)
541+ changeSlidingDirection ( . clockwise)
542+ }
543+ }
544+ else if fullCircle && isSliding && ( angle - newAngle > REVOLUTIONS_DETECTION_THRESHOLD) {
545+ changeSlidingDirection ( . clockwise)
546+ }
547+ else {
548+ changeSlidingDirection ( . counterclockwise)
549+ }
550+ }
551+
552+ if !isSliding {
553+ isSliding = true
554+ }
555+
556+ moveHandle ( newAngle: newAngle)
469557
470558 castDelegate? . circularSlider ( self , valueChangedTo: currentValue, fromUser: true )
471559
@@ -481,6 +569,12 @@ public class MSCircularSlider: UIControl {
481569 snapHandle ( )
482570
483571 handle. isPressed = false
572+ isSliding = false
573+
574+ if slidingDirection != . none {
575+ slidingDirection = . none
576+ castDelegate? . circularSlider ( self , directionChangedTo: slidingDirection)
577+ }
484578
485579 setNeedsDisplay ( )
486580 }
@@ -505,7 +599,7 @@ public class MSCircularSlider: UIControl {
505599 private func drawLabels( ctx: CGContext ) {
506600 if labels. count > 0 {
507601 let attributes = [ NSAttributedString . Key. font: labelFont, NSAttributedString . Key. foregroundColor: labelColor] as [ NSAttributedString . Key : Any ]
508-
602+
509603 for i in 0 ..< labels. count {
510604 let label = labels [ i] as NSString
511605 let labelFrame = frameForLabelAt ( i)
@@ -743,7 +837,7 @@ public class MSCircularSlider: UIControl {
743837 }
744838
745839 /** Snaps the handle to the nearest label/marker depending on the settings */
746- private func snapHandle( ) {
840+ internal func snapHandle( ) {
747841 // Snapping calculation
748842 var fixedAngle = 0.0 as CGFloat
749843
@@ -766,7 +860,6 @@ public class MSCircularSlider: UIControl {
766860 newAngle = degreesToLbl != 0 || !fullCircle ? maximumAngle - degreesToLbl : 0
767861 minDist = abs ( fixedAngle - degreesToLbl)
768862 }
769-
770863 }
771864
772865 currentValue = valueFrom ( angle: newAngle)
@@ -787,7 +880,6 @@ public class MSCircularSlider: UIControl {
787880 }
788881
789882 setNeedsDisplay ( )
790-
791883 }
792884
793885 //================================================================================
0 commit comments