7 Commits

Author SHA1 Message Date
Jalal Ouraigua c3cf5527fd Added Carthage support 2019-01-11 04:35:03 +00:00
Jalal Ouraigua ad0cf786c8 Add Carthage support 2019-01-11 04:14:36 +00:00
Jalal Ouraigua ea35c92b6a Progress 2018-12-23 18:28:27 +00:00
Jalal Ouraigua 1490573852 Fix Slide's value rounding issue 2018-12-22 19:21:05 +00:00
Jalal Ouraigua 68f31bfa1f Remove isNegative. No longer needed 2018-12-22 05:49:20 +00:00
Jalal Ouraigua 6086b3bad3 Update podspec 2018-12-22 05:13:08 +00:00
Jalal Ouraigua 42dbab3b89 - Change the circularSlider's value to function as the generic UIKit slider's value
- Enhance min/max values to support any range positive, negative or both.
- Enhance the label's text display by specifying decimal places or completely override it and set your own text.
2018-12-22 04:46:20 +00:00
13 changed files with 207 additions and 97 deletions
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
@@ -87,7 +87,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="6nV-kU-kJg">
<rect key="frame" x="41.5" y="565" width="331" height="257"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.86000001430511475" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="Yng-5x-Nhy" customClass="DesignableSlider" customModule="JOCircularSlider_Example" customModuleProvider="target">
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.85999999999999999" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="Yng-5x-Nhy" customClass="DesignableSlider" customModule="JOCircularSlider_Example" customModuleProvider="target">
<rect key="frame" x="-2" y="0.0" width="335" height="16"/>
<color key="tintColor" red="0.20392156859999999" green="0.23529411759999999" blue="0.28235294119999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="maximumTrackTintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.6</string>
<string>2.0.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
@@ -78,7 +78,8 @@ class ViewController: UIViewController {
circularSlider.startAngle = direction.startAngle
circularSlider.endAngle = direction.endAngle
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.circularSlider.setValue(self.circularSlider.value, isPercentage: true)
let value = self.circularSlider.value
self.circularSlider.value = value
}
}
@@ -111,8 +112,8 @@ class ViewController: UIViewController {
circularSlider.knobMiddleCircleMultiplier = newMultiplier
}
circularSlider.setValue(circularSlider.value, isPercentage: true)
let value = circularSlider.value
circularSlider.value = value
}
@IBAction private func changeDotCount(_ sender: UISlider) {
@@ -135,6 +136,10 @@ class ViewController: UIViewController {
@IBAction func handleValueChanged(_ sender: CircularSlider) {
print("value: \(sender.value)")
// If you need to override the formatting of the text in the
// label, then use this.
// circularSlider.setLabelText(String(format: "%.3f", sender.value))
}
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2A42614483C3321D5589EF03C68BD21C"
BuildableName = "JOCircularSlider.framework"
BlueprintName = "JOCircularSlider"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2A42614483C3321D5589EF03C68BD21C"
BuildableName = "JOCircularSlider.framework"
BlueprintName = "JOCircularSlider"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.6</string>
<string>2.0.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.6</string>
<string>2.0.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.6</string>
<string>2.0.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'JOCircularSlider'
s.version = '1.0.6'
s.version = '2.0.3'
s.summary = 'A highly customisable and reusable iOS circular slider.'
# This description is used to generate tags and improve search results.
+63 -73
View File
@@ -227,21 +227,24 @@ open class CircularSlider: UIControl {
set { renderer.miniDotView.highlightColor = newValue ? highlightColor : nil }
}
@IBInspectable open var minimumValue: CGFloat {
get { return CGFloat(renderer.minimumValue) }
/**
This value will be pinned to minimumValue/maximumValue
The default value of this property is 0.0.
*/
@IBInspectable open var value: CGFloat { // always min <= value <= max
get { return CGFloat(renderer.value) * (maximumValue - minimumValue) + minimumValue }
set {
renderer.minimumValue = Float(newValue)
setValue(value, isPercentage: true)
guard maximumValue >= minimumValue else {
fatalError("`maximumValue` should be greater then `minimumValue`.")
}
let rangedValue = min(maximumValue, max(minimumValue, newValue))
renderer.value = Float((rangedValue - minimumValue) / (maximumValue - minimumValue))
}
}
@IBInspectable open var maximumValue: CGFloat {
get { return CGFloat(renderer.maximumValue) }
set {
renderer.maximumValue = Float(newValue)
setValue(value, isPercentage: true)
}
}
@IBInspectable open var minimumValue: CGFloat = 0
@IBInspectable open var maximumValue: CGFloat = 100
@IBInspectable open var startAngle: CGFloat {
get { return renderer.startAngle.toDegree }
@@ -258,34 +261,29 @@ open class CircularSlider: UIControl {
set { renderer.isClockwise = newValue }
}
@IBInspectable open var isNegative: Bool {
get { return renderer.isNegative }
set { renderer.isNegative = newValue }
}
@IBInspectable open var labelDecimalPlaces: Int = 0
// MARK: - Private Properties
lazy private var renderer = Renderer(with: self)
private var lastTouchAngle: CGFloat = 0
open private (set) var value: Float = 0 // 0 <= value <= 1
private var centerPoint: CGPoint { return CGPoint(x: bounds.midX, y: bounds.midY) }
// MARK: - Functions
override public init(frame: CGRect) {
super.init(frame: frame)
setValue(renderer.minimumValue)
value = minimumValue
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setValue(renderer.minimumValue)
value = minimumValue
}
override open func layoutSubviews() {
super.layoutSubviews()
renderer.update(bounds, value: value)
renderer.updateUI(in: bounds)
}
// MARK: - Touches
@@ -313,10 +311,9 @@ open class CircularSlider: UIControl {
let angelDeltaAsPercentage = Float(angelDelta / angleRange)
guard abs(angelDeltaAsPercentage) < 1 else { return }
let newValue = isClockwise ? value + angelDeltaAsPercentage : value - angelDeltaAsPercentage
setValue(newValue, isPercentage: true)
let newValue = renderer.value + (isClockwise ? angelDeltaAsPercentage : -angelDeltaAsPercentage)
renderer.value = max(0, min(newValue, 1))
//sendActions(for: .valueChanged)
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
@@ -326,36 +323,20 @@ open class CircularSlider: UIControl {
// MARK: - Public
open func setValue(_ newValue: Float, isPercentage: Bool = false) {
guard maximumValue > minimumValue else {
fatalError("`maximumValue` should be greater then `minimumValue`.")
}
if isPercentage {
value = min(1, max(0, newValue))
} else {
// convert to percentage
let rangedValue = min(renderer.maximumValue, max(renderer.minimumValue, newValue))
value = (rangedValue - renderer.minimumValue) / (renderer.maximumValue - renderer.minimumValue)
}
// updates
renderer.updateText(with: value)
renderer.updatePointerView(in: bounds, value: value)
renderer.maxiDotView.updateColors(using: value)
sendActions(for: .valueChanged)
}
open func setTextFont(named: String, textColor: UIColor, multiplier: CGFloat) {
open func setLabelFont(named: String, textColor: UIColor, multiplier: CGFloat) {
textFontSizeMultiplier = multiplier
let textSize = bounds.height * multiplier
renderer.setTextFont(named: named, textColor: textColor, textSize: textSize)
}
open func setTextShadow(color: UIColor, opacity: Float = 1, offset: CGSize = CGSize(width: 1, height: 1), radius: CGFloat = 0) {
open func setLabelShadow(color: UIColor, opacity: Float = 1, offset: CGSize = CGSize(width: 1, height: 1), radius: CGFloat = 0) {
renderer.setTextShadow(color: color, opacity: opacity, offset: offset, radius: radius)
}
open func setLabelText(_ text: String?) {
renderer.textField.text = text
}
}
class Renderer {
@@ -376,11 +357,6 @@ class Renderer {
maxiDotView.isClockwise = isClockwise
}
}
fileprivate var isNegative: Bool = false {
didSet {
}
}
fileprivate var textFieldIsHidden: Bool = false
fileprivate var fontSizeMultiplier: CGFloat = 0.5
@@ -401,9 +377,18 @@ class Renderer {
}
}
fileprivate var minimumValue: Float = 0.0
fileprivate var maximumValue: Float = 100.0
fileprivate var value: Float = 0.0 { // 0 <= renderer.value <= 1
didSet {
// updates
updateText()
if let bounds = circularSlider?.bounds {
updatePointerView(in: bounds)
}
maxiDotView.updateColors(using: value)
circularSlider?.sendActions(for: .valueChanged)
}
}
// MARK: - Init
@@ -412,8 +397,8 @@ class Renderer {
commonInit()
}
func update(_ bounds: CGRect, value: Float) {
updatePointerView(in: bounds, value: value)
func updateUI(in bounds: CGRect) {
updatePointerView(in: bounds)
updateTextField(in: bounds)
updateDotViews(in: bounds)
}
@@ -469,7 +454,7 @@ private extension Renderer {
textField.layer.cornerRadius = 3
textField.textAlignment = .center
textField.text = ""
textField.keyboardType = .numberPad
textField.keyboardType = .numbersAndPunctuation
textField.keyboardAppearance = .dark
textField.clearsOnInsertion = true
textField.adjustsFontSizeToFitWidth = true
@@ -492,13 +477,13 @@ private extension Renderer {
@objc func keyboardDoneButtonTapped() {
textField.endEditing(true)
guard let circularSlider = circularSlider else { return }
guard let text = textField.text, let newValue = Int(text) else {
updateText(with: circularSlider.value, isPercentage: true)
updateText()
return
}
circularSlider.setValue(Float(newValue))
circularSlider.sendActions(for: .editingDidEnd)
circularSlider?.value = CGFloat(newValue)
circularSlider?.sendActions(for: .editingDidEnd)
//maxiDotView.setNeedsLayout()
}
@@ -529,23 +514,28 @@ private extension Renderer {
textField.isHidden = textFieldIsHidden
}
func updateText(with newValue: Float, isPercentage: Bool = true) {
var value = Int(newValue)
if isPercentage {
// convert value (0...1) to a value within proposed range
value = Int(newValue * (maximumValue - minimumValue) + minimumValue)
func updateText() {
guard let circularSlider = circularSlider else { return }
let minimum = Float(circularSlider.minimumValue)
let maximum = Float(circularSlider.maximumValue)
let _value = Float(circularSlider.value)
var decimalPlaces = circularSlider.labelDecimalPlaces
if value != 0 && _value.rounded(.towardZero) == 0 && decimalPlaces == 0 {
decimalPlaces = 2
}
if isNegative {
value = Int(Float(value) - maximumValue)
}
switch value {
case Int(minimumValue): textField.text = "MIN"
case Int(maximumValue): textField.text = "MAX"
default: textField.text = "\(value)"
let newValue = _value.roundedDown(toPlaces: decimalPlaces)
switch newValue {
case minimum: textField.text = "MIN"
case maximum: textField.text = "MAX"
default: textField.text = String(format: "%.\(decimalPlaces)f", newValue)
}
}
func updatePointerView(in bounds: CGRect, value: Float) {
func updatePointerView(in bounds: CGRect) {
let angleRange = angleDifferenceInDegree(from: startAngle.toDegree, to: endAngle.toDegree, isClockwise: isClockwise)
let angle = isClockwise ? startAngle.toDegree + (angleRange * CGFloat(value)) : startAngle.toDegree - (angleRange * CGFloat(value))
@@ -24,6 +24,13 @@ extension FloatingPoint {
let degree = self * 180 / .pi
return degree >= 0 ? degree : 360 + degree
}
public func roundedDown(toPlaces places: Int) -> Self {
let rounded = self.rounded(.down)
let power = Self(Int(powf(10, Float(places))))
let decimal = ((self - rounded) * power).rounded(.down)
return rounded + (decimal / power)
}
}
extension CGPoint {
+2
View File
@@ -44,6 +44,8 @@ class KnobView: UIView {
}
}
var shadowOpacity: Float = 0.4 {
didSet {
middleCircleShadow.shadowOpacity = shadowOpacity
+45 -15
View File
@@ -35,6 +35,12 @@ it, simply add the following line to your Podfile:
pod 'JOCircularSlider'
```
JOCircularSlider is also available through [Carthage](https://github.com/Carthage/Carthage). To install it, simply add the following line to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile):
``` ruby
github "ouraigua/JOCircularSlider"
```
## Usage
1. Visually:
@@ -57,34 +63,58 @@ circularSlider.isClockwise = false
```
These are just few of the many params you can configure for the slider.
The slider's `value` property is read-only.
The slider's min/max values are IBInspectable and default to 0.0 and 100.0 respectively.
They also support negative and positive values or a mixture of them.
```swift
/**
returns a value in the range 0.0 (minimumValue) to 1.0 (maximumValue).
The default value of this property is 0.0.
*/
open private (set) var value: Float = 0
@IBInspectable open var minimumValue: CGFloat = 0
@IBInspectable open var maximumValue: CGFloat = 100
```
To set the slider's value use the following:
The slider's `value` property is both { get set } and it is designed to work similarly to the familiar value property of UIKit's generic UISlider.
```swift
/**
Set the `value`.
- parameter newValue: if `isPercentage` then new value must be in the range 0.0 to 1.0
- parameter isPercentage: specifies if newValue is in the range [0.0, 1.0] or not
This value will be pinned to minimumValue/maximumValue
The default value of this property is 0.0.
*/
open func setValue(_ newValue: Float, isPercentage: Bool = false)
@IBInspectable open var value: CGFloat // { get set }
```
In order to get value change notifications, use the `Target-Action` pattern which is an inherent part of UIControl, like so:
``` swift
circularSlider.addTarget(target: Any?, action: Selector, for: UIControl.valueChanged)
/**
Add target/action for particular event.
- parameter target: The object whose action method is called
- parameter action: A selector identifying the action method to be called
- parameter Event: The control-specific events for which the action method is called
So far I've only implemented the following:
UIControl.Event.valueChanged
UIControl.Event.editingDidBegin
UIControl.Event.editingDidEnd
*/
circularSlider.addTarget(target: Any?, action: Selector, for: UIControl.Event)
```
To Control the text's appearance, use these:
To Control the appearance of the label displaying the value, use these:
```swift
open func setTextFont(named: String, textColor: UIColor, multiplier: CGFloat)
open func setTextShadow(color: UIColor, opacity: Float = 1, offset: CGSize = CGSize(width: 1, height: 1), radius: CGFloat = 0)
open func setLabelFont(named: String, textColor: UIColor, multiplier: CGFloat)
open func setLabelShadow(color: UIColor, opacity: Float = 1, offset: CGSize = CGSize(width: 1, height: 1), radius: CGFloat = 0)
```
You can also specify the number of decimal places this label should show.
The default is 0.0 unless for example the value is within [0.0, 1.0]
```swift
@IBInspectable open var labelDecimalPlaces: Int = 0
```
If you need to override the text in the label, then just implement the following function inside the selector of the `Target-Action`
method for the event `UIControl.Event.valueChanged`
```swift
open func setLabelText(_ text: String?)
```
## References