diff --git a/.DS_Store b/.DS_Store index 7d919f8..b0aff99 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/LiveKnob.podspec b/LiveKnob.podspec index 2f4ed7b..a006332 100644 --- a/LiveKnob.podspec +++ b/LiveKnob.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "LiveKnob" - s.version = "0.0.4" + s.version = "0.0.5" s.summary = "Yet another knob for iOS but with IBDesignable and Ableton Live style." s.swift_version = "4.2" diff --git a/LiveKnob.xcodeproj/project.pbxproj b/LiveKnob.xcodeproj/project.pbxproj index 2ced9ab..5af3e2f 100644 --- a/LiveKnob.xcodeproj/project.pbxproj +++ b/LiveKnob.xcodeproj/project.pbxproj @@ -94,7 +94,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0940; + LastUpgradeCheck = 1220; ORGANIZATIONNAME = cemolcay; TargetAttributes = { B2D75A5B203D2B2D000D7D09 = { @@ -193,6 +193,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -218,7 +219,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -253,6 +254,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -272,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -288,7 +290,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 77Y3N48SNF; INFOPLIST_FILE = LiveKnob/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.cemolcay.LiveKnob; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -304,7 +306,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 77Y3N48SNF; INFOPLIST_FILE = LiveKnob/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.cemolcay.LiveKnob; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/LiveKnob/LiveKnob.swift b/LiveKnob/LiveKnob.swift index eab49c9..40edbac 100644 --- a/LiveKnob/LiveKnob.swift +++ b/LiveKnob/LiveKnob.swift @@ -9,6 +9,14 @@ import UIKit import UIKit.UIGestureRecognizerSubclass +/// The marker views around the knob. +public class LiveKnobMarker: UIView { + /// Offset of the markers from the knob base. + public var markerOffset: CGFloat = 24 + /// Custom transform for the marker. + public var markerTransform: CGAffineTransform? +} + /// Knob controlling type. Rotating or horizontal and/or vertical touching. public enum LiveKnobControlType: Int, Codable { /// Only horizontal sliding changes the knob's value. @@ -25,13 +33,13 @@ public enum LiveKnobControlType: Int, Codable { /// Whether changes in the value of the knob generate continuous update events. /// Defaults `true`. @IBInspectable public var continuous = true - + /// The minimum value of the knob. Defaults to 0.0. @IBInspectable public var minimumValue: Float = 0.0 { didSet { drawKnob() }} - + /// The maximum value of the knob. Defaults to 1.0. @IBInspectable public var maximumValue: Float = 1.0 { didSet { drawKnob() }} - + /// Value of the knob. Also known as progress. @IBInspectable public var value: Float = 0.0 { didSet { @@ -39,25 +47,27 @@ public enum LiveKnobControlType: Int, Codable { setNeedsLayout() } } - + /// Default color for the ring base. Defaults black. @IBInspectable public var baseColor: UIColor = .black { didSet { drawKnob() }} - + /// Default color for the pointer. Defaults black. @IBInspectable public var pointerColor: UIColor = .black { didSet { drawKnob() }} - + /// Default color for the progress. Defaults orange. @IBInspectable public var progressColor: UIColor = .orange { didSet { drawKnob() }} - + /// Line width for the ring base. Defaults 2. @IBInspectable public var baseLineWidth: CGFloat = 2 { didSet { drawKnob() }} - + /// Line width for the progress. Defaults 2. @IBInspectable public var progressLineWidth: CGFloat = 2 { didSet { drawKnob() }} - + /// Line width for the pointer. Defaults 2. @IBInspectable public var pointerLineWidth: CGFloat = 2 { didSet { drawKnob() }} - + + /// The marker views around the knob. + public var markers = [LiveKnobMarker]() { didSet { refreshMarkersIfNeeded() }} /// Layer for the base ring. public private(set) var baseLayer = CAShapeLayer() /// Layer for the progress ring. @@ -72,19 +82,19 @@ public enum LiveKnobControlType: Int, Codable { public var controlType: LiveKnobControlType = .rotary /// Knob gesture recognizer. public private(set) var gestureRecognizer: LiveKnobGestureRecognizer! - + // MARK: Init - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonInit() } - + public override init(frame: CGRect) { super.init(frame: frame) commonInit() } - + private func commonInit() { // Setup knob gesture gestureRecognizer = LiveKnobGestureRecognizer(target: self, action: #selector(handleGesture(_:))) @@ -97,14 +107,15 @@ public enum LiveKnobControlType: Int, Codable { layer.addSublayer(progressLayer) layer.addSublayer(pointerLayer) } - + // MARK: Lifecycle - + public override func layoutSubviews() { super.layoutSubviews() drawKnob() + layoutMarkersIfNeeded() } - + public func drawKnob() { // Setup layers baseLayer.bounds = bounds @@ -119,39 +130,39 @@ public enum LiveKnobControlType: Int, Codable { baseLayer.strokeColor = baseColor.cgColor progressLayer.strokeColor = progressColor.cgColor pointerLayer.strokeColor = pointerColor.cgColor - + // Draw base ring. let center = CGPoint(x: baseLayer.bounds.width / 2, y: baseLayer.bounds.height / 2) let radius = (min(baseLayer.bounds.width, baseLayer.bounds.height) / 2) - baseLineWidth let ring = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) baseLayer.path = ring.cgPath baseLayer.lineCap = .round - + // Draw pointer. let pointer = UIBezierPath() pointer.move(to: center) pointer.addLine(to: CGPoint(x: center.x + radius, y: center.y)) pointerLayer.path = pointer.cgPath pointerLayer.lineCap = .round - + let angle = CGFloat(angleForValue(value)) let progressRing = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: angle, clockwise: true) progressLayer.path = progressRing.cgPath progressLayer.lineCap = .round - + // Draw pointer CATransaction.begin() CATransaction.setDisableActions(true) pointerLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1) CATransaction.commit() } - + // MARK: Rotation Gesture Recogniser - + // note the use of dynamic, because calling // private swift selectors(@ gestureRec target:action:!) gives an exception @objc dynamic func handleGesture(_ gesture: LiveKnobGestureRecognizer) { - + switch controlType { case .horizontal: value += Float(gesture.diagonalChange.width) * (maximumValue - minimumValue) @@ -162,18 +173,18 @@ public enum LiveKnobControlType: Int, Codable { value -= Float(gesture.diagonalChange.height) * (maximumValue - minimumValue) case .rotary: let midPointAngle = (2 * CGFloat.pi + startAngle - endAngle) / 2 + endAngle - + var boundedAngle = gesture.touchAngle if boundedAngle > midPointAngle { boundedAngle -= 2 * CGFloat.pi } else if boundedAngle < (midPointAngle - 2 * CGFloat.pi) { boundedAngle += 2 * CGFloat.pi } - + boundedAngle = min(endAngle, max(startAngle, boundedAngle)) value = min(maximumValue, max(minimumValue, valueForAngle(boundedAngle))) } - + // Inform changes based on continuous behaviour of the knob. if continuous { sendActions(for: .valueChanged) @@ -182,20 +193,24 @@ public enum LiveKnobControlType: Int, Codable { sendActions(for: .valueChanged) } } - + + if gesture.state == .began { + sendActions(for: .editingDidBegin) + } + if gesture.state == .ended { sendActions(for: .editingDidEnd) } } - + // MARK: Value/Angle conversion - + public func valueForAngle(_ angle: CGFloat) -> Float { let angleRange = Float(endAngle - startAngle) let valueRange = maximumValue - minimumValue return Float(angle - startAngle) / angleRange * valueRange + minimumValue } - + public func angleForValue(_ value: Float) -> CGFloat { let angleRange = endAngle - startAngle let valueRange = CGFloat(maximumValue - minimumValue) @@ -213,62 +228,128 @@ public class LiveKnobGestureRecognizer: UIPanGestureRecognizer { private var lastTouchPoint: CGPoint = .zero /// Horizontal and vertical slide sensitivity multiplier. Defaults 0.005. public var slidingSensitivity: CGFloat = 0.005 - + // MARK: UIGestureRecognizerSubclass - + public override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) - + state = .began + // Update diagonal movement. guard let touch = touches.first else { return } lastTouchPoint = touch.location(in: view) - + // Update rotary movement. updateTouchAngleWithTouches(touches) } - + public override func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) - + state = .changed + // Update diagonal movement. guard let touchPoint = touches.first?.location(in: view) else { return } diagonalChange.width = (touchPoint.x - lastTouchPoint.x) * slidingSensitivity diagonalChange.height = (touchPoint.y - lastTouchPoint.y) * slidingSensitivity - + // Reset last touch point. lastTouchPoint = touchPoint - + // Update rotary movement. updateTouchAngleWithTouches(touches) } - + public override func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) state = .ended } - + private func updateTouchAngleWithTouches(_ touches: Set) { guard let touch = touches.first else { return } let touchPoint = touch.location(in: view) touchAngle = calculateAngleToPoint(touchPoint) } - + private func calculateAngleToPoint(_ point: CGPoint) -> CGFloat { let centerOffset = CGPoint(x: point.x - view!.bounds.midX, y: point.y - view!.bounds.midY) return atan2(centerOffset.y, centerOffset.x) } - + // MARK: Lifecycle - + public init() { super.init(target: nil, action: nil) maximumNumberOfTouches = 1 minimumNumberOfTouches = 1 } - + public override init(target: Any?, action: Selector?) { super.init(target: target, action: action) maximumNumberOfTouches = 1 minimumNumberOfTouches = 1 } } + +extension LiveKnob { + + func refreshMarkersIfNeeded() { + for sub in subviews { + if sub is LiveKnobMarker { + sub.removeFromSuperview() + } + } + for marker in markers { + addSubview(marker) + } + } + + func layoutMarkersIfNeeded() { + for (index, marker) in markers.enumerated() { + let angle = radians(from: self.angle(for: marker, at: index)) + + let x = cos(angle) + let y = sin(angle) + + marker.transform = .identity + var rect = marker.frame + let radius = (min(baseLayer.bounds.width, baseLayer.bounds.height) / 2) - baseLineWidth + marker.markerOffset + + let newX = CGFloat(x) * radius + center.x + let newY = CGFloat(y) * radius + center.y + + rect.origin.x = newX - marker.frame.width / 2 + rect.origin.y = newY - marker.frame.height / 2 + marker.frame = rect + marker.transform = marker.markerTransform ?? CGAffineTransform(rotationAngle: angle) + } + } + + func angle(for marker: UIView, at index: Int) -> Float { + let percentage: Float = ((100.0 / Float(markers.count - 1)) * Float(index)) / 100.0 + return degrees(for: percentage, from: degrees(from: startAngle), to: degrees(from: endAngle)) + } + + func degrees(for percentage: Float, from startAngle: Float, to endAngle: Float) -> Float { + if endAngle > startAngle { + return startAngle + (endAngle - startAngle) * percentage + } else { + return startAngle + (360.0 + endAngle - startAngle) * percentage + } + } + + public func degrees(from radian: CGFloat) -> Float { + return degrees(from: Float(radian)) + } + + public func degrees(from radian: Float) -> Float { + return radian * (180 / Float.pi) + } + + func radians(from degree: Float) -> CGFloat { + return radians(from: CGFloat(degree)) + } + + func radians(from degree: CGFloat) -> CGFloat { + return degree * .pi / 180 + } +} diff --git a/LiveKnob/ViewController.swift b/LiveKnob/ViewController.swift index 9d97487..3054d95 100644 --- a/LiveKnob/ViewController.swift +++ b/LiveKnob/ViewController.swift @@ -17,8 +17,17 @@ class ViewController: UIViewController { guard let knob = knob, let knobLabel = knobLabel else { return } knobLabel.text = String(format: "%.2f", arguments: [knob.value]) knob.controlType = .horizontalAndVertical + knob.markers = [Int](0..<8).map({ _ in createMarker() }) } - + + func createMarker() -> LiveKnobMarker { + let view = LiveKnobMarker(frame: CGRect(origin: .zero, size: CGSize(width: 8, height: 8))) + view.isUserInteractionEnabled = false + view.backgroundColor = .lightGray + view.layer.cornerRadius = 4 + return view + } + @IBAction func knobValueDidChange(sender: LiveKnob) { knobLabel?.text = String(format: "%.2f", arguments: [sender.value]) } diff --git a/README.md b/README.md index 8cb364d..3c22c05 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ You can change the line width and color of the base ring, progress ring and poin ### LiveKnobControlType You can set the `controlType` for changing the knob's touch control behaviour. It supports horizontal and/or vertical slidings as well as rotary slidings. +### LiveKnobMarker +You can create custom marker views in with `LiveKnobMarker` type and set them to LiveKnob's `markers` array in order to draw markers around the knob. You can set individual offset and transform for each marker as well. + AppStore ----