Last active
November 12, 2019 11:45
-
-
Save Coder-ACJHP/9dd7e6fe27d57073b2fd6a809e681f6a to your computer and use it in GitHub Desktop.
UICBalanceSlider : designable vertical balance slider for Swift.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Created by Hitendra Solanki on 04/17/2018. Updated by Coder ACJHP | |
// Copyright (c) 2018 Hitendra Solanki. All rights reserved. | |
// | |
import UIKit | |
public class UICBalanceSlider: UIControl { | |
enum HSProgressType { | |
case left(progress: Double) | |
case right(progress: Double) | |
} | |
var type: EffectType = .none | |
var mainVCdelegate: SubEffectsPanelDelegate! | |
var feedbackGenerator: UIImpactFeedbackGenerator? | |
//MARK:- IBOutlets | |
@IBOutlet private var contentView: UIView! | |
@IBOutlet internal weak var thumb: UIView! | |
@IBOutlet private weak var thumbImageView: UIImageView! | |
@IBOutlet public weak var sliderTrackView: UIView! | |
@IBOutlet private weak var leftProgressView: UIView! | |
@IBOutlet private weak var rightProgressView: UIView! | |
@IBOutlet weak var bottomBorderView: UIView! | |
@IBOutlet internal weak var constaintThumbCenterX: NSLayoutConstraint! | |
@IBOutlet internal weak var constrintLeftProgressWidth: NSLayoutConstraint! | |
@IBOutlet internal weak var constrintRightProgressWidth: NSLayoutConstraint! | |
//MARK:- iVars | |
internal var cornersRadius: CGFloat = 8 | |
internal var isMovingThumb = false; | |
internal let thumbHalfWidth: Double = 15 | |
override public var tintColor: UIColor! { | |
didSet{ | |
super.tintColor = tintColor; | |
self.leftProgressView.backgroundColor = tintColor | |
self.rightProgressView.backgroundColor = tintColor | |
} | |
} | |
public var rangeValue: CRange? | |
public var value: Double = 0.0 { | |
didSet{ | |
if self.value < self.internalRightHalfRangeConverter!.range1.low { | |
self.setProgressOnUI(progressType: HSProgressType.left(progress: (value))) | |
}else{ | |
self.setProgressOnUI(progressType: HSProgressType.right(progress: (value))) | |
} | |
} | |
} | |
public var thumbImage: UIImage? { | |
didSet { | |
self.thumb.backgroundColor = .clear | |
self.thumbImageView.image = thumbImage | |
} | |
} | |
//MARK:- Constructors | |
public init() { | |
super.init(frame: .zero) | |
self.commonInit() | |
} | |
public override init(frame: CGRect) { | |
super.init(frame: frame) | |
self.commonInit() | |
} | |
public required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
self.commonInit() | |
} | |
func commonInit() { | |
Bundle.main.loadNibNamed(UICBalanceSlider.className, owner: self, options: nil) | |
self.contentView.frame = self.frame | |
self.addSubview(self.contentView) | |
self.backgroundColor = UIColor.clear | |
self.contentView.backgroundColor = UIColor.clear | |
self.thumb.backgroundColor = UIColor.white | |
self.thumb.layer.cornerRadius = 30/2 | |
self.thumb.layer.masksToBounds = true | |
self.thumb.addShadow(elevation: 2.0) | |
self.thumbImage = UIImage(named: "img_vs_thumb") | |
self.leftProgressView.backgroundColor = tintColor | |
self.rightProgressView.backgroundColor = tintColor | |
self.sliderTrackView.layer.masksToBounds = true | |
self.sliderTrackView.layer.borderColor = UIColor.init(hexString: "#e7e7e7").cgColor | |
self.sliderTrackView.layer.borderWidth = 0.4 | |
self.sliderTrackView.layer.cornerRadius = self.cornersRadius | |
self.tintColor = UIColor.init(hexString: "#4B8CDC") | |
self.bottomBorderView.layer.cornerRadius = self.cornersRadius | |
self.bottomBorderView.layer.masksToBounds = true | |
self.rangeValue = CRange(low: -100, high: 100) //default Range for the slider | |
self.value = 0; //default progress value | |
} | |
func setDefaultSliderValue() { | |
switch type { | |
default: | |
self.set(value: 0.5, animated: false) | |
mainVCdelegate.equalSliderValue() | |
} | |
} | |
func setSliderValue(val : Float) { | |
self.set(value: Double(val), animated: true) | |
mainVCdelegate.equalSliderValue() | |
} | |
func setSliderState(val : Bool) { | |
if !val { | |
UIView.animate(withDuration: 0.3) { | |
self.contentView.subviews.forEach { $0.layer.opacity = 0.7 } | |
} | |
} else { | |
UIView.animate(withDuration: 0.3) { | |
self.contentView.subviews.forEach { $0.layer.opacity = 1.0 } | |
} | |
} | |
self.isEnabled = val | |
} | |
} | |
//MARK:- Update Progress Value | |
extension UICBalanceSlider { | |
public func set(value: Double, animated: Bool) { | |
self.value = self.rangeValue!.innerValue(value: value); | |
UIView.animate(withDuration: animated ? 0.25 : 0.0) { | |
self.layoutIfNeeded() | |
} | |
} | |
internal func setProgressOnUI(progressType: HSProgressType){ | |
switch progressType { | |
case .left(let progress): | |
let constantValue = min( | |
(self.internalLeftHalfRangeConverter!.range2.high.cgfloat - self.internalLeftHalfRangeConverter!.toRange2(of: progress).cgfloat), | |
(self.centerX - self.thumbHalfWidth).cgfloat | |
) | |
self.constrintRightProgressWidth.constant = 0.0; | |
self.constrintLeftProgressWidth.constant = constantValue | |
self.constaintThumbCenterX.constant = -1 * constantValue | |
case .right(let progress): | |
let constantValue = min( | |
self.internalRightHalfRangeConverter!.toRange2(of: progress).cgfloat - self.internalRightHalfRangeConverter!.range2.low.cgfloat, | |
(self.centerX - self.thumbHalfWidth).cgfloat | |
) | |
self.constrintLeftProgressWidth.constant = 0.0; | |
self.constrintRightProgressWidth.constant = constantValue | |
self.constaintThumbCenterX.constant = constantValue | |
} | |
} | |
} | |
//MARK:- Internal Converters | |
extension UICBalanceSlider { | |
internal var internalRangeConverter: CRangeCalculator? { | |
guard let rangeValue = rangeValue else { | |
return nil; | |
} | |
return | |
CRangeCalculator(range1: CRange(low: rangeValue.low, high: rangeValue.high), | |
range2: CRange(low: 0, high: self.bounds.size.width.double)) | |
} | |
internal var internalLeftHalfRangeConverter: CRangeCalculator? { | |
guard let internalConverter = self.internalRangeConverter else { | |
return nil; | |
} | |
return | |
CRangeCalculator(range1: CRange(low: internalConverter.range1.low, high: internalConverter.range1.mid), | |
range2: CRange(low: internalConverter.toRange2(of: internalConverter.range1.low), high: internalConverter.toRange2(of: internalConverter.range1.mid)) | |
) | |
} | |
internal var internalRightHalfRangeConverter: CRangeCalculator? { | |
guard let internalConverter = self.internalRangeConverter else { | |
return nil; | |
} | |
return | |
CRangeCalculator(range1: CRange(low: internalConverter.range1.mid, high: internalConverter.range1.high), | |
range2: CRange(low: internalConverter.toRange2(of: internalConverter.range1.mid), high: internalConverter.toRange2(of: internalConverter.range1.high)) | |
) | |
} | |
} | |
//MARK:- Touches Event + Geometry | |
extension UICBalanceSlider { | |
internal var centerX: Double { | |
return Double(self.bounds.size.width) / 2.0 | |
} | |
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
guard let movingPoint = touches.first?.location(in: self) else { return } | |
self.isMovingThumb = self.hitTest(movingPoint, with: event) == self.thumb | |
// Instantiate a new generator. | |
feedbackGenerator = UIImpactFeedbackGenerator(style: .light) | |
// Prepare the generator when the gesture begins. | |
feedbackGenerator?.prepare() | |
} | |
@objc func hideSliderValueLabelIfNeeded() { | |
self.mainVCdelegate.sliderCannotSendStopEvent() | |
} | |
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { | |
guard let movingPoint = touches.first?.location(in: self) else { return } | |
if isMovingThumb { | |
let value = self.internalRangeConverter!.toRange1(of: Double(movingPoint.x)) | |
self.value = self.rangeValue!.innerValue(value: value) | |
mainVCdelegate.sliderChanged(effectType: type, strength: CGFloat(self.value)) | |
let currentValue = Int(self.value * 100) | |
if currentValue == 50 { | |
feedbackGenerator?.impactOccurred() | |
} | |
} | |
self.perform(#selector(self.hideSliderValueLabelIfNeeded), with: nil, afterDelay: 2.5) | |
} | |
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { | |
self.isMovingThumb = false | |
mainVCdelegate.sliderDragFinished(effectType: type, strength: CGFloat(self.value)) | |
// Release the current generator. | |
feedbackGenerator = nil | |
} | |
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { | |
self.isMovingThumb = false | |
mainVCdelegate.sliderDragFinished(effectType: type, strength: CGFloat(self.value)) | |
// Release the current generator. | |
feedbackGenerator = nil | |
} | |
} | |
extension Double { | |
var cgfloat: CGFloat { | |
return CGFloat(self) | |
} | |
} | |
extension CGFloat { | |
var double: Double { | |
return Double(self) | |
} | |
} | |
//MARK:- Shadow | |
internal protocol UIViewOptions { | |
func addShadow(elevation: Double) | |
} | |
extension UIView: UIViewOptions { | |
internal func addShadow(elevation: Double) { | |
self.layer.masksToBounds = false | |
self.layer.shadowColor = UIColor.black.cgColor | |
self.layer.shadowOffset = CGSize(width: 0, height: elevation) | |
self.layer.shadowRadius = abs(CGFloat(elevation)) | |
self.layer.shadowOpacity = 0.24 | |
} | |
} | |
// | |
// HSRange.swift | |
// HSRange | |
// | |
// Created by Hitendra Solanki on 05/07/17. | |
// Copyright Β© 2017 Hitendra Solanki. All rights reserved. | |
// | |
import Foundation | |
//MARK:- Backbone structure behind the scene | |
public struct CRange { | |
public var low: Double! | |
public var high: Double! | |
public init(low: Double, high: Double) { | |
self.low = low | |
self.high = high | |
} | |
} | |
//MARK:- Helpers | |
public extension CRange { | |
//middle value of the range | |
//e.g. Range(-100,100), this will return 0 | |
public var mid: Double { | |
return (self.low + self.high)/2.0 | |
} | |
//this returns the value inside of range | |
//e.g. Range(-100,100), if you pass -200, this will return -100 | |
public func innerValue(value: Double) -> Double { | |
return min(max(value,self.low),self.high) | |
} | |
} | |
//MARK:- Converter | |
public struct CRangeCalculator { | |
public var range1: CRange! | |
public var range2: CRange! | |
public init(range1: CRange, range2: CRange) { | |
self.range1 = range1 | |
self.range2 = range2 | |
} | |
//Helper functions to convert value of range1 to range2 | |
public func toRange2(of value: Double) -> Double { | |
let result = self.convert(a: self.range1.low!, b: self.range1.high!, | |
value: value, | |
c: self.range2.low!, d: self.range2.high!); | |
return self.range2.innerValue(value: result) | |
} | |
//Helper functions to convert value of range2 to range1 | |
public func toRange1(of value: Double) -> Double { | |
let result = self.convert(a: self.range2.low!, b: self.range2.high!, | |
value: value, | |
c: self.range1.low!, d: self.range1.high!); | |
return self.range1.innerValue(value: result) | |
} | |
//Core logic behind conversion | |
internal func convert(a: Double, b: Double, value: Double, c: Double, d: Double) -> Double{ | |
let v1 = (value - a) * (d - c); | |
let v2 = v1 / (b - a) | |
return v2 + c | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> | |
<device id="retina4_7" orientation="portrait"> | |
<adaptation id="fullscreen"/> | |
</device> | |
<dependencies> | |
<deployment version="2304" identifier="iOS"/> | |
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/> | |
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | |
</dependencies> | |
<objects> | |
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="UICBalanceSlider" customModule="FitbestMakeup" customModuleProvider="target"> | |
<connections> | |
<outlet property="bottomBorderView" destination="gm2-RD-PxT" id="yqU-IK-p83"/> | |
<outlet property="constaintThumbCenterX" destination="Ht8-sB-w60" id="1db-6I-k3D"/> | |
<outlet property="constrintLeftProgressWidth" destination="pTx-og-79R" id="0i9-lM-X1q"/> | |
<outlet property="constrintRightProgressWidth" destination="ezs-J1-msH" id="n2h-Cj-kpp"/> | |
<outlet property="contentView" destination="iN0-l3-epB" id="oly-1c-epr"/> | |
<outlet property="leftProgressView" destination="Tdq-O5-LpE" id="gpp-ao-2lu"/> | |
<outlet property="rightProgressView" destination="jsp-ZF-ToP" id="TEn-c3-wau"/> | |
<outlet property="sliderTrackView" destination="Adz-4H-i3t" id="aSw-c4-Yta"/> | |
<outlet property="thumb" destination="E0x-EJ-8Wi" id="NJh-jK-hJN"/> | |
<outlet property="thumbImageView" destination="Cmh-Ux-Afg" id="vvJ-MG-Lo9"/> | |
</connections> | |
</placeholder> | |
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> | |
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="iN0-l3-epB"> | |
<rect key="frame" x="0.0" y="0.0" width="375" height="113"/> | |
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | |
<subviews> | |
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BsY-kY-1i4" userLabel="Middle tick view"> | |
<rect key="frame" x="186.5" y="42.5" width="2" height="8"/> | |
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | |
<constraints> | |
<constraint firstAttribute="height" constant="8" id="A3U-AN-fWZ"/> | |
<constraint firstAttribute="width" constant="2" id="rIe-bM-8sH"/> | |
</constraints> | |
</view> | |
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gm2-RD-PxT" userLabel="Border View"> | |
<rect key="frame" x="0.0" y="46.5" width="375" height="20"/> | |
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | |
<constraints> | |
<constraint firstAttribute="height" constant="20" id="qNi-CD-QMu"/> | |
</constraints> | |
</view> | |
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Adz-4H-i3t" userLabel="progress container"> | |
<rect key="frame" x="2" y="48.5" width="371" height="16"/> | |
<subviews> | |
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Tdq-O5-LpE" userLabel="left progress"> | |
<rect key="frame" x="37.5" y="2" width="150" height="12"/> | |
<color key="backgroundColor" red="0.99215686274509807" green="0.12549019607843137" blue="0.23137254901960785" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | |
<constraints> | |
<constraint firstAttribute="width" constant="150" id="pTx-og-79R"/> | |
</constraints> | |
</view> | |
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jsp-ZF-ToP" userLabel="right progress"> | |
<rect key="frame" x="187.5" y="2" width="150" height="12"/> | |
<color key="backgroundColor" red="0.99215686270000003" green="0.12549019610000001" blue="0.23137254900000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | |
<constraints> | |
<constraint firstAttribute="width" constant="150" id="ezs-J1-msH"/> | |
</constraints> | |
</view> | |
</subviews> | |
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | |
<constraints> | |
<constraint firstAttribute="height" constant="16" id="E0D-ZF-fAH"/> | |
<constraint firstItem="Tdq-O5-LpE" firstAttribute="top" secondItem="Adz-4H-i3t" secondAttribute="top" constant="2" id="Ivg-GE-kwi"/> | |
<constraint firstAttribute="bottom" secondItem="Tdq-O5-LpE" secondAttribute="bottom" constant="2" id="avI-A9-Bir"/> | |
<constraint firstAttribute="bottom" secondItem="jsp-ZF-ToP" secondAttribute="bottom" constant="2" id="b1e-16-FJQ"/> | |
<constraint firstItem="jsp-ZF-ToP" firstAttribute="leading" secondItem="Adz-4H-i3t" secondAttribute="centerX" constant="2" id="kLH-2d-XGk"/> | |
<constraint firstAttribute="centerX" secondItem="Tdq-O5-LpE" secondAttribute="trailing" constant="-2" id="wOX-Xi-KcK"/> | |
<constraint firstItem="jsp-ZF-ToP" firstAttribute="top" secondItem="Adz-4H-i3t" secondAttribute="top" constant="2" id="yzF-Sj-gEm"/> | |
</constraints> | |
</view> | |
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="E0x-EJ-8Wi"> | |
<rect key="frame" x="170" y="39" width="35" height="35"/> | |
<subviews> | |
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Cmh-Ux-Afg"> | |
<rect key="frame" x="0.0" y="0.0" width="35" height="35"/> | |
</imageView> | |
</subviews> | |
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | |
<gestureRecognizers/> | |
<constraints> | |
<constraint firstAttribute="height" constant="35" id="6S4-MJ-tc2"/> | |
<constraint firstAttribute="trailing" secondItem="Cmh-Ux-Afg" secondAttribute="trailing" id="84A-jg-4Y0"/> | |
<constraint firstAttribute="bottom" secondItem="Cmh-Ux-Afg" secondAttribute="bottom" id="Bi9-cd-3aH"/> | |
<constraint firstItem="Cmh-Ux-Afg" firstAttribute="top" secondItem="E0x-EJ-8Wi" secondAttribute="top" id="bas-XJ-FPW"/> | |
<constraint firstAttribute="width" constant="35" id="moT-m5-aig"/> | |
<constraint firstItem="Cmh-Ux-Afg" firstAttribute="leading" secondItem="E0x-EJ-8Wi" secondAttribute="leading" id="pBd-vb-aum"/> | |
</constraints> | |
</view> | |
</subviews> | |
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | |
<constraints> | |
<constraint firstItem="Adz-4H-i3t" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="3G5-Wk-VYf"/> | |
<constraint firstItem="gm2-RD-PxT" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="4Cj-1Z-YBx"/> | |
<constraint firstAttribute="trailing" secondItem="Adz-4H-i3t" secondAttribute="trailing" constant="2" id="BsN-MU-JCB"/> | |
<constraint firstItem="E0x-EJ-8Wi" firstAttribute="centerX" secondItem="Adz-4H-i3t" secondAttribute="centerX" id="Ht8-sB-w60"/> | |
<constraint firstItem="BsY-kY-1i4" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="IJK-uD-9aw"/> | |
<constraint firstItem="gm2-RD-PxT" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="IYg-eH-Zuy"/> | |
<constraint firstItem="E0x-EJ-8Wi" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="LSZ-ta-ZzE"/> | |
<constraint firstItem="BsY-kY-1i4" firstAttribute="bottom" secondItem="iN0-l3-epB" secondAttribute="centerY" constant="-6" id="h7U-Fa-YyG"/> | |
<constraint firstItem="Adz-4H-i3t" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="2" id="jyE-5Z-ZEl"/> | |
<constraint firstItem="gm2-RD-PxT" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="qtf-HZ-1aU"/> | |
<constraint firstItem="Adz-4H-i3t" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="sWm-ki-pGN"/> | |
<constraint firstAttribute="trailing" secondItem="gm2-RD-PxT" secondAttribute="trailing" id="zmh-uT-0u1"/> | |
</constraints> | |
<nil key="simulatedTopBarMetrics"/> | |
<nil key="simulatedBottomBarMetrics"/> | |
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/> | |
<point key="canvasLocation" x="-37.600000000000001" y="-156.97151424287858"/> | |
</view> | |
</objects> | |
</document> |
@Coder-ACJHP I have updated the Original Component as per your request,
HSCenterSlider
has now two supported classes HSHorizontalCenterSlider
and HSVerticleCenterSlider
, which shared the common code and UI component for the calculations.
Available at https://github.com/hitendradeveloper/HSCenterSlider/
Version: 2.0.1
Thanks for the feature request.
@Coder-ACJHP I have updated the Original Component as per your request,
HSCenterSlider
has now two supported classesHSHorizontalCenterSlider
andHSVerticleCenterSlider
, which shared the common code and UI component for the calculations.Available at https://github.com/hitendradeveloper/HSCenterSlider/
Version: 2.0.1Thanks for the feature request.
Perfect ππ»ππ»ππ»ππ»
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How to use ?
Note: This component orginally created by Hitendra Solanki and it was horizontally, I updated, changed some properties of it and maked it vertically.