9 Commits

Author SHA1 Message Date
Jalal Ouraigua 7fdd18f890 Merge remote-tracking branch 'refs/remotes/origin/master' 2018-12-22 05:51:50 +00:00
Jalal Ouraigua ea3ce73f51 Remove isNegative. No longer needed 2018-12-22 05:51:09 +00:00
Jalal Ouraigua 556f8a858c Update README.md 2018-12-22 05:08:48 +00:00
Jalal Ouraigua ef28529778 - 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:59:38 +00:00
Jalal Ouraigua b3c83b0daf Update version 2018-12-10 05:08:52 +00:00
Jalal Ouraigua cb8fc1f889 Add value change Target-Action 2018-12-10 04:52:18 +00:00
Jalal Ouraigua eb18fac98b Update codebeat config 2018-12-09 05:10:14 +00:00
Jalal Ouraigua ae1febd06c Update gitignore 2018-12-09 00:10:38 +00:00
Jalal Ouraigua 04181154f3 Update README 2018-12-09 00:05:40 +00:00
9 changed files with 139 additions and 115 deletions
+1 -1
View File
@@ -5,6 +5,6 @@
"ARITY" : [5, 6, 7, 8],
"ABC": [15, 25, 50, 70],
"TOTAL_COMPLEXITY": [100, 180, 280, 400],
"TOTAL_LOC": [200, 250, 320, 450]
"TOTAL_LOC": [250, 300, 350, 450]
}
}
+3
View File
@@ -35,3 +35,6 @@ Carthage/Build
# `pod install` in .travis.yml
#
# Pods/
*.podspec
Podfile*
-29
View File
@@ -1,29 +0,0 @@
#
# Be sure to run `pod lib lint KnobFramework.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'KnobFramework'
s.version = '1.0.1'
s.summary = 'A highly customisable and reusable iOS circular slider.'
s.description = <<-DESC
KnobFramework is a highly customisable and reusable iOS circular slider that mimics the behaviour of a knob control. It uses no preset images and every one of its components is drawn completely in code making it extremely adaptable to every design and theme.
It's written in Swift 4.2 and it's 100% IBDesignable and all parameters are IBInspectable.
You can control almost every aspect of the slider's design: Size, colors, direction (clockwise/anti-clockwise), etc...
DESC
s.homepage = 'https://github.com/ouraigua/KnobFramework'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Jalal Ouraigua' => 'ouraigua@icloud.com' }
s.source = { :git => 'https://github.com/ouraigua/KnobFramework.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/ouraigua'
s.ios.deployment_target = '12.0'
s.source_files = 'KnobFramework/**/*.{swift}'
s.documentation_url = 'http://ouraigua.com/github/jocircularslider/docs/index.html'
end
+5 -5
View File
@@ -15,11 +15,11 @@
AF4F27DE21BC3DD000D39C60 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4F27D721BC3DD000D39C60 /* Extensions.swift */; };
AF4F27DF21BC3DD000D39C60 /* PointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4F27D821BC3DD000D39C60 /* PointerView.swift */; };
AF4F27E021BC3DD000D39C60 /* DotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4F27D921BC3DD000D39C60 /* DotView.swift */; };
AF4F27E421BC6A1F00D39C60 /* KnobFramework.podspec in Resources */ = {isa = PBXBuildFile; fileRef = AF4F27E321BC6A1F00D39C60 /* KnobFramework.podspec */; };
AF4F27E921BC6A8200D39C60 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AF4F27E521BC6A8200D39C60 /* .gitignore */; };
AF4F27EA21BC6A8200D39C60 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = AF4F27E621BC6A8200D39C60 /* LICENSE */; };
AF4F27EB21BC6A8200D39C60 /* .codebeatsettings in Resources */ = {isa = PBXBuildFile; fileRef = AF4F27E721BC6A8200D39C60 /* .codebeatsettings */; };
AF4F27EC21BC6A8200D39C60 /* .swift-version in Resources */ = {isa = PBXBuildFile; fileRef = AF4F27E821BC6A8200D39C60 /* .swift-version */; };
AF4F280E21BC94A100D39C60 /* KnobFramework.podspec in Resources */ = {isa = PBXBuildFile; fileRef = AF4F280D21BC94A100D39C60 /* KnobFramework.podspec */; };
FDE1C785BDA3FFA82992A815 /* libPods-KnobFramework.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F7FDC350CB218A84F96BF04E /* libPods-KnobFramework.a */; };
/* End PBXBuildFile section */
@@ -35,11 +35,11 @@
AF4F27D821BC3DD000D39C60 /* PointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointerView.swift; sourceTree = "<group>"; };
AF4F27D921BC3DD000D39C60 /* DotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotView.swift; sourceTree = "<group>"; };
AF4F27E221BC693000D39C60 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
AF4F27E321BC6A1F00D39C60 /* KnobFramework.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = KnobFramework.podspec; sourceTree = "<group>"; };
AF4F27E521BC6A8200D39C60 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
AF4F27E621BC6A8200D39C60 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
AF4F27E721BC6A8200D39C60 /* .codebeatsettings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .codebeatsettings; sourceTree = "<group>"; };
AF4F27E821BC6A8200D39C60 /* .swift-version */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ".swift-version"; sourceTree = "<group>"; };
AF4F280D21BC94A100D39C60 /* KnobFramework.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = KnobFramework.podspec; sourceTree = "<group>"; };
CD06525455A889F9C77DF83D /* Pods-KnobFramework.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KnobFramework.release.xcconfig"; path = "Pods/Target Support Files/Pods-KnobFramework/Pods-KnobFramework.release.xcconfig"; sourceTree = "<group>"; };
E79A965E70FE5C13B3DAFCB6 /* Pods-KnobFramework.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KnobFramework.debug.xcconfig"; path = "Pods/Target Support Files/Pods-KnobFramework/Pods-KnobFramework.debug.xcconfig"; sourceTree = "<group>"; };
F7FDC350CB218A84F96BF04E /* libPods-KnobFramework.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-KnobFramework.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -89,8 +89,8 @@
isa = PBXGroup;
children = (
AF4F27CB21BC3B3200D39C60 /* KnobFramework.h */,
AF4F27D421BC3DD000D39C60 /* CircularLayer.swift */,
AF4F27D321BC3DD000D39C60 /* CircularSlider.swift */,
AF4F27D421BC3DD000D39C60 /* CircularLayer.swift */,
AF4F27D521BC3DD000D39C60 /* DotLayer.swift */,
AF4F27D921BC3DD000D39C60 /* DotView.swift */,
AF4F27D721BC3DD000D39C60 /* Extensions.swift */,
@@ -104,7 +104,7 @@
AF4F27E121BC691A00D39C60 /* Podspec Metadata */ = {
isa = PBXGroup;
children = (
AF4F27E321BC6A1F00D39C60 /* KnobFramework.podspec */,
AF4F280D21BC94A100D39C60 /* KnobFramework.podspec */,
AF4F27E221BC693000D39C60 /* README.md */,
AF4F27E621BC6A8200D39C60 /* LICENSE */,
AF4F27E721BC6A8200D39C60 /* .codebeatsettings */,
@@ -194,9 +194,9 @@
files = (
AF4F27EA21BC6A8200D39C60 /* LICENSE in Resources */,
AF4F27E921BC6A8200D39C60 /* .gitignore in Resources */,
AF4F27E421BC6A1F00D39C60 /* KnobFramework.podspec in Resources */,
AF4F27EB21BC6A8200D39C60 /* .codebeatsettings in Resources */,
AF4F27EC21BC6A8200D39C60 /* .swift-version in Resources */,
AF4F280E21BC94A100D39C60 /* KnobFramework.podspec in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+72 -58
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,29 +261,29 @@ open class CircularSlider: UIControl {
set { renderer.isClockwise = 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
@@ -291,6 +294,7 @@ open class CircularSlider: UIControl {
let distanceToOrigin = location - centerPoint
lastTouchAngle = atan2(distanceToOrigin.y, distanceToOrigin.x).toDegree
sendActions(for: .editingDidBegin)
}
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
@@ -307,40 +311,32 @@ 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))
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
sendActions(for: .editingDidEnd)
}
// 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)
}
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 {
@@ -381,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
@@ -392,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)
}
@@ -472,13 +477,14 @@ 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))
maxiDotView.layoutSubviews()
circularSlider?.value = CGFloat(newValue)
circularSlider?.sendActions(for: .editingDidEnd)
//maxiDotView.setNeedsLayout()
}
// MARK: - Updates
@@ -508,20 +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
}
switch value {
case Int(minimumValue): textField.text = "MIN"
case Int(maximumValue): textField.text = "MAX"
default: textField.text = "\(value)"
guard let newValue = Float(String(format: "%.\(decimalPlaces)f", _value)) else { return }
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))
+1
View File
@@ -162,3 +162,4 @@ private extension DotView {
}
}
}
+8
View File
@@ -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 {
@@ -53,3 +60,4 @@ extension CALayer {
return UIBezierPath(ovalIn: bounds.insetBy(dx: bounds.width * multiplier, dy: bounds.height * multiplier))
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>2.0.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+48 -21
View File
@@ -38,9 +38,6 @@ pod 'KnobFramework'
## Usage
1. Visually:
```swift
import KnobFramework
```
Drag a UIView to your storyboard, change its class of to CircularSlider and start visually customising the design to your liking.
All the parameters are IBInspectable, so you can configure the slider straight from the attribute inspector tab, without having to write a single line of code.
@@ -60,28 +57,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
```
To set the slider's value use the following:
```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
*/
open func setValue(_ newValue: Float, isPercentage: Bool = false)
@IBInspectable open var minimumValue: CGFloat = 0
@IBInspectable open var maximumValue: CGFloat = 100
```
To Control the text's appearance, use these:
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
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)
/**
This value will be pinned to minimumValue/maximumValue
The default value of this property is 0.0.
*/
@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
/**
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 appearance of the label displaying the value, use these:
```swift
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