Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fdd18f890 | |||
| ea3ce73f51 | |||
| 556f8a858c | |||
| ef28529778 | |||
| b3c83b0daf | |||
| cb8fc1f889 | |||
| eb18fac98b | |||
| ae1febd06c | |||
| 04181154f3 | |||
| d44c2519b4 | |||
| 001dbd7871 | |||
| bf53a2decb | |||
| 5bac4820e0 | |||
| 83176a8c16 |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"SWIFT": {
|
||||
"TOO_MANY_IVARS": [8, 12, 16, 20],
|
||||
"TOO_MANY_FUNCTIONS": [46, 55, 65, 85],
|
||||
"ARITY" : [5, 6, 7, 8],
|
||||
"ABC": [15, 25, 50, 70],
|
||||
"TOTAL_COMPLEXITY": [100, 180, 280, 400],
|
||||
"TOTAL_LOC": [250, 300, 350, 450]
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# OS X
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
*.xccheckout
|
||||
profile
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
|
||||
# Bundler
|
||||
.bundle
|
||||
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
|
||||
#
|
||||
# Note: if you ignore the Pods directory, make sure to uncomment
|
||||
# `pod install` in .travis.yml
|
||||
#
|
||||
# Pods/
|
||||
|
||||
*.podspec
|
||||
Podfile*
|
||||
@@ -0,0 +1 @@
|
||||
4.2
|
||||
@@ -1,39 +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.0'
|
||||
s.summary = 'A highly customisable and reusable iOS circular slider.'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
# * Try to keep it short, snappy and to the point.
|
||||
# * Write the description between the DESC delimiters below.
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
|
||||
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.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
|
||||
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 = '10.0'
|
||||
|
||||
s.source_files = 'KnobFramework/**/*.{swift}'
|
||||
|
||||
s.documentation_url = 'http://ouraigua.com/github/jocircularslider/docs/index.html'
|
||||
|
||||
end
|
||||
@@ -15,6 +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 */; };
|
||||
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 */
|
||||
|
||||
@@ -29,6 +34,12 @@
|
||||
AF4F27D721BC3DD000D39C60 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -58,6 +69,7 @@
|
||||
AF4F27BE21BC3B3200D39C60 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF4F27E121BC691A00D39C60 /* Podspec Metadata */,
|
||||
AF4F27CA21BC3B3200D39C60 /* KnobFramework */,
|
||||
AF4F27C921BC3B3200D39C60 /* Products */,
|
||||
39B98C47AA8B4792DF7C299D /* Pods */,
|
||||
@@ -77,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 */,
|
||||
@@ -89,6 +101,19 @@
|
||||
path = KnobFramework;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AF4F27E121BC691A00D39C60 /* Podspec Metadata */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF4F280D21BC94A100D39C60 /* KnobFramework.podspec */,
|
||||
AF4F27E221BC693000D39C60 /* README.md */,
|
||||
AF4F27E621BC6A8200D39C60 /* LICENSE */,
|
||||
AF4F27E721BC6A8200D39C60 /* .codebeatsettings */,
|
||||
AF4F27E521BC6A8200D39C60 /* .gitignore */,
|
||||
AF4F27E821BC6A8200D39C60 /* .swift-version */,
|
||||
);
|
||||
name = "Podspec Metadata";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B381CC5598F6BFBA4C33DBA7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -167,6 +192,11 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AF4F27EA21BC6A8200D39C60 /* LICENSE in Resources */,
|
||||
AF4F27E921BC6A8200D39C60 /* .gitignore in Resources */,
|
||||
AF4F27EB21BC6A8200D39C60 /* .codebeatsettings in Resources */,
|
||||
AF4F27EC21BC6A8200D39C60 /* .swift-version in Resources */,
|
||||
AF4F280E21BC94A100D39C60 /* KnobFramework.podspec in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -162,3 +162,4 @@ private extension DotView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<string>2.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2018 Jalal Ouraigua <ouraigua@icloud.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -3,7 +3,7 @@ platform :ios, '12.0'
|
||||
|
||||
target 'KnobFramework' do
|
||||
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
|
||||
#use_frameworks!
|
||||
# use_frameworks!
|
||||
|
||||
# Pods for KnobFramework
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||

|
||||
|
||||
[](https://cocoapods.org/pods/KnobFramework)
|
||||
[](https://cocoapods.org/pods/KnobFramework)
|
||||
[]()
|
||||
[](https://cocoapods.org/pods/KnobFramework)
|
||||
[](https://twitter.com/ouraigua)
|
||||
[](http://clayallsopp.github.io/readme-score?url=https://github.com/ouraigua/knobframework)
|
||||
[](https://codebeat.co/projects/github-com-ouraigua-knobframework-master)
|
||||
|
||||
# KnobFramework
|
||||
|
||||
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.
|
||||
|
||||
## Example
|
||||
|
||||
To run the example project, clone the repo, and run `pod install` from the Example directory first.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
- iOS 10.0+
|
||||
- Xcode 10.0
|
||||
|
||||
## Installation
|
||||
|
||||
KnobFramework is available through [CocoaPods](https://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'KnobFramework'
|
||||
```
|
||||
|
||||
## Usage
|
||||
1. Visually:
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
2. Programatically:
|
||||
|
||||
```swift
|
||||
import KnobFramework
|
||||
|
||||
let circularSlider = CircularSlider(frame: aFrame)
|
||||
circularSlider.startAngle = 230
|
||||
circularSlider.endAngle = 310
|
||||
circularSlider.minimumValue = 0
|
||||
circularSlider.maximumValue = 60
|
||||
circularSlider.isClockwise = false
|
||||
```
|
||||
These are just few of the many params you can configure for the slider.
|
||||
|
||||
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
|
||||
@IBInspectable open var minimumValue: CGFloat = 0
|
||||
@IBInspectable open var maximumValue: CGFloat = 100
|
||||
```
|
||||
|
||||
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
|
||||
/**
|
||||
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
|
||||
|
||||
The project is Inspired by:
|
||||
- [How To Make a Custom Control Tutorial: A Reusable Knob](https://www.raywenderlich.com/5294-how-to-make-a-custom-control-tutorial-a-reusable-knob)
|
||||
- [HGCircularSlider](https://github.com/HamzaGhazouani/HGCircularSlider)
|
||||
|
||||
## Author
|
||||
|
||||
Jalal Ouraigua, ouraigua@icloud.com
|
||||
|
||||
## License
|
||||
|
||||
KnobFramework is available under the MIT license. See the LICENSE file for more info.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 722 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 598 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 835 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Reference in New Issue
Block a user