Skip to content

Commit 23c1228

Browse files
Mohamed ShahawyMohamed Shahawy
authored andcommitted
Added multiple new features and fixed some bugs (check the CHANGELOG)
1 parent b2d7302 commit 23c1228

File tree

17 files changed

+675
-157
lines changed

17 files changed

+675
-157
lines changed

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.0
1+
5.0

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@ The changelog for `MSCircularSlider`. Summarized release notes can be found in t
33

44
------------------------
55

6+
## 1.3.0 - 02-07-2019
7+
#### Added
8+
- An option to bound the handle at 100% (after a given number of revolutions) and 0%, preventing endless looping
9+
- `MSDoubleHandleCircularSlider` can now snap to labels and markers
10+
- A new optional protocol method to specify the sliding direction (`clockwise`, `counterclockwise`, or `none`)
11+
- `sliderPadding` property to overcome components getting clipped (thanks to Noiibe!)
12+
- A new optional protocol method that detects how many revolutions have passed (valid only when the slider is a full circle (`maximumAngle` = 360.0))
13+
14+
15+
#### Changed
16+
- Minor changes to the example project to illustrate the new sliding direction and revolutions' counter methods
17+
18+
#### Fixed
19+
- Swift 5.0 support
20+
- Handle-image rotation bug (thanks to Noiibe!)
21+
- `radius` member's value not getting updated
22+
23+
24+
## 1.2.3 - 11-11-2018
25+
#### Merged
26+
- Swift 4.2 support (PR by ivanbarisic05)
27+
628
## 1.2.2 - 19-06-2018
729
#### Added
830
- An option to rotate the handle image to always point outwards

MSCircularSlider.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
Pod::Spec.new do |s|
22
s.name = 'MSCircularSlider'
3-
s.version = '1.2.2'
3+
s.version = '1.3.0'
44
s.license = { :type => 'MIT', :file => 'LICENSE' }
55
s.authors = { 'ThunderStruct' => '[email protected]' }
66
s.summary = 'A full-featured circular slider for iOS applications'
77
s.homepage = 'https://github.com/ThunderStruct/MSCircularSlider'
88

99
# Source Info
1010
s.platform = :ios, '9.3'
11-
s.source = { :git => 'https://github.com/ThunderStruct/MSCircularSlider.git', :branch => "master", :tag => "1.2.2" }
11+
s.source = { :git => 'https://github.com/ThunderStruct/MSCircularSlider.git', :branch => "master", :tag => "1.3.0" }
1212
s.source_files = 'MSCircularSlider/*.{swift}'
1313

1414
s.requires_arc = true

MSCircularSlider/MSCircularSlider+IB.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,29 @@ extension MSCircularSlider {
5454
}
5555
}
5656

57+
@IBInspectable public var _sliderPadding: CGFloat {
58+
get {
59+
return sliderPadding
60+
}
61+
set {
62+
sliderPadding = newValue
63+
}
64+
}
65+
66+
@IBInspectable public var _maximumRevolutions: Int {
67+
get {
68+
return maximumRevolutions
69+
}
70+
set {
71+
if newValue < 0 {
72+
maximumRevolutions = -1
73+
}
74+
else {
75+
maximumRevolutions = newValue
76+
}
77+
}
78+
}
79+
5780
@IBInspectable public var _lineWidth: Int {
5881
get {
5982
return lineWidth

MSCircularSlider/MSCircularSlider.swift

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2123
extension 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
//================================================================================

MSCircularSlider/MSCircularSliderHandle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public class MSCircularSliderHandle: CALayer {
201201
bitmap?.rotate(by: CGFloat(toRad(Double(degrees))));
202202

203203
// Now, draw the rotated/scaled image into the context
204-
bitmap?.draw(img.cgImage!, in: CGRect(x: -img.size.width / 2, y: -img.size.height / 2, width: img.size.width, height: img.size.height))
204+
bitmap?.draw(img.cgImage!, in: CGRect(x: -rotatedSize.width / 2, y: -rotatedSize.height / 2, width: rotatedSize.width, height: rotatedSize.height))
205205

206206
let newImage = UIGraphicsGetImageFromCurrentImageContext()
207207
UIGraphicsEndImageContext()

0 commit comments

Comments
 (0)