diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c86dbf..33d7ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ The changelog for `MSCircularSlider`. Summarized release notes can be found in t ------------------------ +## 1.2.2 - 19-06-2018 +#### Added + - An option to rotate the handle image to always point outwards + +#### Changed + - Minor structural improvement + ## 1.2.1 - 09-02-2018 #### Fixed - A setter conflict occuring on earlier Swift versions diff --git a/MSCircularSlider.podspec b/MSCircularSlider.podspec index d88b915..39194ba 100755 --- a/MSCircularSlider.podspec +++ b/MSCircularSlider.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MSCircularSlider' - s.version = '1.2.1' + s.version = '1.2.2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.authors = { 'ThunderStruct' => 'mohamedshahawy@aucegypt.edu' } s.summary = 'A full-featured circular slider for iOS applications' @@ -8,7 +8,7 @@ Pod::Spec.new do |s| # Source Info s.platform = :ios, '9.3' - s.source = { :git => 'https://github.com/ThunderStruct/MSCircularSlider.git', :branch => "master", :tag => "1.2.1" } + s.source = { :git => 'https://github.com/ThunderStruct/MSCircularSlider.git', :branch => "master", :tag => "1.2.2" } s.source_files = 'MSCircularSlider/*.{swift}' s.requires_arc = true diff --git a/MSCircularSlider/MSCircularSlider+IB.swift b/MSCircularSlider/MSCircularSlider+IB.swift index 00a0207..5f303b3 100644 --- a/MSCircularSlider/MSCircularSlider+IB.swift +++ b/MSCircularSlider/MSCircularSlider+IB.swift @@ -141,6 +141,15 @@ extension MSCircularSlider { } } + @IBInspectable public var _handleRotatable: Bool { + get { + return handleRotatable + } + set { + handleRotatable = newValue + } + } + //================================================================================ // LABELS PROPERTIES //================================================================================ diff --git a/MSCircularSlider/MSCircularSlider.swift b/MSCircularSlider/MSCircularSlider.swift index baac791..21aaf75 100755 --- a/MSCircularSlider/MSCircularSlider.swift +++ b/MSCircularSlider/MSCircularSlider.swift @@ -221,6 +221,16 @@ public class MSCircularSlider: UIControl { } } + /** Specifies whether or not the handle should rotate to always point outwards - *default: false* */ + public var handleRotatable: Bool { + set { + handle.isRotatable = newValue + } + get { + return handle.isRotatable + } + } + /** The calculated handle's diameter based on its type */ public var handleDiameter: CGFloat { return handle.diameter @@ -416,9 +426,10 @@ public class MSCircularSlider: UIControl { drawLabels(ctx: ctx!) // Rotate slider - self.transform = getRotationalTransform() + let rotationalTransform = getRotationalTransform() + self.transform = rotationalTransform for view in subviews { // cancel rotation on all subviews added by the user - view.transform = getRotationalTransform().inverted() + view.transform = rotationalTransform.inverted() } } @@ -832,7 +843,6 @@ public class MSCircularSlider: UIControl { return transform } else { - if let rotation = self.rotationAngle { return CGAffineTransform.identity.rotated(by: CGFloat(toRad(Double(rotation)))) } diff --git a/MSCircularSlider/MSCircularSliderHandle.swift b/MSCircularSlider/MSCircularSliderHandle.swift index b0dbf3c..25d3312 100755 --- a/MSCircularSlider/MSCircularSliderHandle.swift +++ b/MSCircularSlider/MSCircularSliderHandle.swift @@ -46,6 +46,13 @@ public class MSCircularSliderHandle: CALayer { } } + /** Specifies whether or not the handle should rotate to always point outwards - *default: false* */ + internal var isRotatable: Bool = false { + didSet { + setNeedsDisplay() + } + } + /** The handle's color - *default: .darkGray* */ internal var color: UIColor = .darkGray { didSet { @@ -123,8 +130,13 @@ public class MSCircularSliderHandle: CALayer { y: center().y - diameter * 0.5, width: diameter, height: diameter) - image?.draw(in: frame) - + if isRotatable { + let rotatedImg = imageRotated(img: image!, byDegrees: angle) + rotatedImg.draw(in: frame) + } + else { + image?.draw(in: frame) + } } else if handleType == .doubleCircle { calculatedHandleColor.withAlphaComponent(isHighlightable && isPressed ? 0.9 : 1.0).set() @@ -159,4 +171,41 @@ public class MSCircularSliderHandle: CALayer { return point.x >= center().x - handleRadius && point.x <= center().x + handleRadius && point.y >= center().y - handleRadius && point.y <= center().y + handleRadius } + /** Converts degrees to radians */ + private func toRad(_ degrees: Double) -> Double { + return ((Double.pi * degrees) / 180.0) + } + + /** Converts radians to degrees */ + private func toDeg(_ radians: Double) -> Double { + return ((180.0 * radians) / Double.pi) + } + + /** Rotates a given image by the specified degrees */ + private func imageRotated(img: UIImage, byDegrees degrees: CGFloat) -> UIImage { + + // calculate the size of the rotated view's containing box for our drawing space + let rotatedViewBox = UIView(frame: CGRect(origin: CGPoint.zero, size: img.size)) + let t = CGAffineTransform(rotationAngle: CGFloat(toRad(Double(degrees)))) + rotatedViewBox.transform = t + let rotatedSize = rotatedViewBox.frame.size + + // Create the bitmap context + UIGraphicsBeginImageContext(rotatedSize) + let bitmap = UIGraphicsGetCurrentContext() + + // Move the origin to the middle of the image so we will rotate and scale around the center. + bitmap?.translateBy(x: rotatedSize.width / 2.0, y: rotatedSize.height / 2.0) + + // // Rotate the image context + bitmap?.rotate(by: CGFloat(toRad(Double(degrees)))); + + // Now, draw the rotated/scaled image into the context + bitmap?.draw(img.cgImage!, in: CGRect(x: -img.size.width / 2, y: -img.size.height / 2, width: img.size.width, height: img.size.height)) + + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage! + } } diff --git a/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift b/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift index ad58644..fe43f0c 100644 --- a/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift +++ b/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift @@ -83,5 +83,13 @@ extension MSDoubleHandleCircularSlider { } } + @IBInspectable public var _secondHandleRotatable: Bool { + get { + return secondHandleRotatable + } + set { + secondHandleRotatable = newValue + } + } } diff --git a/MSCircularSlider/MSDoubleHandleCircularSlider.swift b/MSCircularSlider/MSDoubleHandleCircularSlider.swift index 1322989..e28d138 100755 --- a/MSCircularSlider/MSDoubleHandleCircularSlider.swift +++ b/MSCircularSlider/MSDoubleHandleCircularSlider.swift @@ -139,6 +139,16 @@ public class MSDoubleHandleCircularSlider: MSCircularSlider { } } + /** Specifies whether or not the second handle should rotate to always point outwards - *default: false* */ + public var secondHandleRotatable: Bool { + set { + secondHandle.isRotatable = newValue + } + get { + return secondHandle.isRotatable + } + } + // CALCULATED MEMBERS /** The calculated second handle's diameter based on its type */ @@ -186,7 +196,7 @@ public class MSDoubleHandleCircularSlider: MSCircularSlider { secondHandle.center = { return self.pointOnCircleAt(angle: self.secondAngle) } - secondHandle.angle = 60 + secondHandle.angle = CGFloat(max(0, 60.0).truncatingRemainder(dividingBy: Double(maximumAngle + 1))) } override init(frame: CGRect) { diff --git a/MSCircularSliderExample/MSCircularSlider/MSCircularSlider+IB.swift b/MSCircularSliderExample/MSCircularSlider/MSCircularSlider+IB.swift index 00a0207..5f303b3 100644 --- a/MSCircularSliderExample/MSCircularSlider/MSCircularSlider+IB.swift +++ b/MSCircularSliderExample/MSCircularSlider/MSCircularSlider+IB.swift @@ -141,6 +141,15 @@ extension MSCircularSlider { } } + @IBInspectable public var _handleRotatable: Bool { + get { + return handleRotatable + } + set { + handleRotatable = newValue + } + } + //================================================================================ // LABELS PROPERTIES //================================================================================ diff --git a/MSCircularSliderExample/MSCircularSlider/MSCircularSlider.swift b/MSCircularSliderExample/MSCircularSlider/MSCircularSlider.swift index baac791..21aaf75 100755 --- a/MSCircularSliderExample/MSCircularSlider/MSCircularSlider.swift +++ b/MSCircularSliderExample/MSCircularSlider/MSCircularSlider.swift @@ -221,6 +221,16 @@ public class MSCircularSlider: UIControl { } } + /** Specifies whether or not the handle should rotate to always point outwards - *default: false* */ + public var handleRotatable: Bool { + set { + handle.isRotatable = newValue + } + get { + return handle.isRotatable + } + } + /** The calculated handle's diameter based on its type */ public var handleDiameter: CGFloat { return handle.diameter @@ -416,9 +426,10 @@ public class MSCircularSlider: UIControl { drawLabels(ctx: ctx!) // Rotate slider - self.transform = getRotationalTransform() + let rotationalTransform = getRotationalTransform() + self.transform = rotationalTransform for view in subviews { // cancel rotation on all subviews added by the user - view.transform = getRotationalTransform().inverted() + view.transform = rotationalTransform.inverted() } } @@ -832,7 +843,6 @@ public class MSCircularSlider: UIControl { return transform } else { - if let rotation = self.rotationAngle { return CGAffineTransform.identity.rotated(by: CGFloat(toRad(Double(rotation)))) } diff --git a/MSCircularSliderExample/MSCircularSlider/MSCircularSliderHandle.swift b/MSCircularSliderExample/MSCircularSlider/MSCircularSliderHandle.swift index b0dbf3c..25d3312 100755 --- a/MSCircularSliderExample/MSCircularSlider/MSCircularSliderHandle.swift +++ b/MSCircularSliderExample/MSCircularSlider/MSCircularSliderHandle.swift @@ -46,6 +46,13 @@ public class MSCircularSliderHandle: CALayer { } } + /** Specifies whether or not the handle should rotate to always point outwards - *default: false* */ + internal var isRotatable: Bool = false { + didSet { + setNeedsDisplay() + } + } + /** The handle's color - *default: .darkGray* */ internal var color: UIColor = .darkGray { didSet { @@ -123,8 +130,13 @@ public class MSCircularSliderHandle: CALayer { y: center().y - diameter * 0.5, width: diameter, height: diameter) - image?.draw(in: frame) - + if isRotatable { + let rotatedImg = imageRotated(img: image!, byDegrees: angle) + rotatedImg.draw(in: frame) + } + else { + image?.draw(in: frame) + } } else if handleType == .doubleCircle { calculatedHandleColor.withAlphaComponent(isHighlightable && isPressed ? 0.9 : 1.0).set() @@ -159,4 +171,41 @@ public class MSCircularSliderHandle: CALayer { return point.x >= center().x - handleRadius && point.x <= center().x + handleRadius && point.y >= center().y - handleRadius && point.y <= center().y + handleRadius } + /** Converts degrees to radians */ + private func toRad(_ degrees: Double) -> Double { + return ((Double.pi * degrees) / 180.0) + } + + /** Converts radians to degrees */ + private func toDeg(_ radians: Double) -> Double { + return ((180.0 * radians) / Double.pi) + } + + /** Rotates a given image by the specified degrees */ + private func imageRotated(img: UIImage, byDegrees degrees: CGFloat) -> UIImage { + + // calculate the size of the rotated view's containing box for our drawing space + let rotatedViewBox = UIView(frame: CGRect(origin: CGPoint.zero, size: img.size)) + let t = CGAffineTransform(rotationAngle: CGFloat(toRad(Double(degrees)))) + rotatedViewBox.transform = t + let rotatedSize = rotatedViewBox.frame.size + + // Create the bitmap context + UIGraphicsBeginImageContext(rotatedSize) + let bitmap = UIGraphicsGetCurrentContext() + + // Move the origin to the middle of the image so we will rotate and scale around the center. + bitmap?.translateBy(x: rotatedSize.width / 2.0, y: rotatedSize.height / 2.0) + + // // Rotate the image context + bitmap?.rotate(by: CGFloat(toRad(Double(degrees)))); + + // Now, draw the rotated/scaled image into the context + bitmap?.draw(img.cgImage!, in: CGRect(x: -img.size.width / 2, y: -img.size.height / 2, width: img.size.width, height: img.size.height)) + + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage! + } } diff --git a/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift b/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift index ad58644..fe43f0c 100644 --- a/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift +++ b/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider+IB.swift @@ -83,5 +83,13 @@ extension MSDoubleHandleCircularSlider { } } + @IBInspectable public var _secondHandleRotatable: Bool { + get { + return secondHandleRotatable + } + set { + secondHandleRotatable = newValue + } + } } diff --git a/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider.swift b/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider.swift index 801e49f..e28d138 100755 --- a/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider.swift +++ b/MSCircularSliderExample/MSCircularSlider/MSDoubleHandleCircularSlider.swift @@ -139,6 +139,16 @@ public class MSDoubleHandleCircularSlider: MSCircularSlider { } } + /** Specifies whether or not the second handle should rotate to always point outwards - *default: false* */ + public var secondHandleRotatable: Bool { + set { + secondHandle.isRotatable = newValue + } + get { + return secondHandle.isRotatable + } + } + // CALCULATED MEMBERS /** The calculated second handle's diameter based on its type */ diff --git a/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.pbxproj b/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.pbxproj index 0e987a5..a5ea34a 100644 --- a/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.pbxproj +++ b/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ D935DC021F81AE7A00E6B6EE /* MarkersLabelsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D935DC011F81AE7A00E6B6EE /* MarkersLabelsVC.swift */; }; D935DC041F81AE8500E6B6EE /* DoubleHandleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D935DC031F81AE8500E6B6EE /* DoubleHandleVC.swift */; }; D935DC061F81AE8E00E6B6EE /* GradientColorsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D935DC051F81AE8E00E6B6EE /* GradientColorsVC.swift */; }; - D953E8F320251CB7007E60B8 /* Icon1024.png in Resources */ = {isa = PBXBuildFile; fileRef = D953E8F220251CB7007E60B8 /* Icon1024.png */; }; D96C34881F813B050041B50C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96C34871F813B050041B50C /* AppDelegate.swift */; }; D96C348A1F813B050041B50C /* ExamplesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96C34891F813B050041B50C /* ExamplesMenu.swift */; }; D96C348D1F813B050041B50C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D96C348B1F813B050041B50C /* Main.storyboard */; }; @@ -40,7 +39,6 @@ D935DC011F81AE7A00E6B6EE /* MarkersLabelsVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkersLabelsVC.swift; sourceTree = ""; }; D935DC031F81AE8500E6B6EE /* DoubleHandleVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoubleHandleVC.swift; sourceTree = ""; }; D935DC051F81AE8E00E6B6EE /* GradientColorsVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientColorsVC.swift; sourceTree = ""; }; - D953E8F220251CB7007E60B8 /* Icon1024.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Icon1024.png; path = "../../../../../../../Desktop/Communicate Misc/Icon1024.png"; sourceTree = ""; }; D96C34841F813B050041B50C /* MSCircularSliderExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MSCircularSliderExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; D96C34871F813B050041B50C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D96C34891F813B050041B50C /* ExamplesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesMenu.swift; sourceTree = ""; }; @@ -88,7 +86,6 @@ D935DC031F81AE8500E6B6EE /* DoubleHandleVC.swift */, D935DC051F81AE8E00E6B6EE /* GradientColorsVC.swift */, D9254D221F852E71006F7A81 /* ColorPickerView.swift */, - D953E8F220251CB7007E60B8 /* Icon1024.png */, D96C348B1F813B050041B50C /* Main.storyboard */, D96C348E1F813B060041B50C /* Assets.xcassets */, D96C34901F813B060041B50C /* LaunchScreen.storyboard */, @@ -174,7 +171,6 @@ D96C34921F813B060041B50C /* LaunchScreen.storyboard in Resources */, D96C348F1F813B060041B50C /* Assets.xcassets in Resources */, D96C348D1F813B050041B50C /* Main.storyboard in Resources */, - D953E8F320251CB7007E60B8 /* Icon1024.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.xcworkspace/xcuserdata/mohamed.xcuserdatad/UserInterfaceState.xcuserstate b/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.xcworkspace/xcuserdata/mohamed.xcuserdatad/UserInterfaceState.xcuserstate index 8e08dc5..7af3a50 100644 Binary files a/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.xcworkspace/xcuserdata/mohamed.xcuserdatad/UserInterfaceState.xcuserstate and b/MSCircularSliderExample/MSCircularSliderExample.xcodeproj/project.xcworkspace/xcuserdata/mohamed.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MSCircularSliderExample/MSCircularSliderExample/Base.lproj/Main.storyboard b/MSCircularSliderExample/MSCircularSliderExample/Base.lproj/Main.storyboard index aabe411..c1dc0f0 100644 --- a/MSCircularSliderExample/MSCircularSliderExample/Base.lproj/Main.storyboard +++ b/MSCircularSliderExample/MSCircularSliderExample/Base.lproj/Main.storyboard @@ -32,6 +32,12 @@ + + + + + + @@ -448,6 +454,10 @@ Rotation Angle: Computed + + + + diff --git a/README.md b/README.md index 8d8b791..8196781 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # MSCircularSlider -[![Build Status](https://travis-ci.org/ThunderStruct/MSCircularSlider.svg?branch=master)](https://travis-ci.org/ThunderStruct/MSCircularSlider) [![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg)](https://github.com/ThunderStruct/MSCircularSlider) [![CocoaPods](https://img.shields.io/badge/pod-1.2.1-blue.svg)](https://cocoapods.org/pods/MSCircularSlider) [![License](https://img.shields.io/cocoapods/l/AFNetworking.svg)](https://github.com/ThunderStruct/MSCircularSlider/blob/master/LICENSE) +[![Build Status](https://travis-ci.org/ThunderStruct/MSCircularSlider.svg?branch=master)](https://travis-ci.org/ThunderStruct/MSCircularSlider) [![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg)](https://github.com/ThunderStruct/MSCircularSlider) [![CocoaPods](https://img.shields.io/badge/pod-1.2.2-blue.svg)](https://cocoapods.org/pods/MSCircularSlider) [![License](https://img.shields.io/cocoapods/l/AFNetworking.svg)](https://github.com/ThunderStruct/MSCircularSlider/blob/master/LICENSE) A fully `IBDesignable` and `IBInspectable` circular slider for iOS applications @@ -86,6 +86,7 @@ view.addSubview(slider!) - `handleEnlargementPoints`: the number of points the handle is larger than lineWidth - default 10 - note: this property only applies to handles of types .largeCircle or .doubleCircle - `handleHighlightable`: indicates whether the handle should _highlight_ (becomes semitransparent) while being pressed - default true + - `handleRotatable`: specifies whether or not the handle should rotate to always point outwards - default false - `labels`: the string array that contains all labels to be displayed in an evenly-distributed manner - default [ ] - note: all changes to this array will not be applied instantly unless they go through the assignment operator (=). To perform changes, use the provided methods below - `labelColor`: the color applied to the displayed labels - default .black @@ -117,6 +118,7 @@ Inherits from MSCircularSlider with the following differences - `secondHandleEnlargementPoints`: the number of points the second handle is larger than lineWidth - default 10 - note: this property only applies to handles of types .largeCircle or .doubleCircle - `secondHandleHighlightable`: indicates whether the second handle should _highlight_ (becomes semitransparent) while being pressed - default true + - `secondHandleRotatable`: specifies whether or not the second handle should rotate to always point outwards - default false - `snapToLabels`: from the super class - overridden and made unavailable - `snapToMarkers`: from the super class - overriden and made unavailable