Skip to content

Instantly share code, notes, and snippets.

@Coder-ACJHP
Last active November 12, 2019 11:45
Show Gist options
  • Save Coder-ACJHP/9dd7e6fe27d57073b2fd6a809e681f6a to your computer and use it in GitHub Desktop.
Save Coder-ACJHP/9dd7e6fe27d57073b2fd6a809e681f6a to your computer and use it in GitHub Desktop.
UICBalanceSlider : designable vertical balance slider for Swift.
// 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
}
}
<?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
Copy link
Author

Coder-ACJHP commented Apr 5, 2019

How to use ?

let balanceSlider = UICBalanceSlider()
balanceSlider.frame.size = CGSize(width: HereWriteSliderHeight! height: AndHereSliderWidth) // Vice versa
view.addSubview(balanceSlider)
balanceSlider.rangeValue = CRange(low: 0.0, high: 1.0)
balanceSlider.center = CGPoint(x: view.frame.width/2, y: view.frame.height/2)
balanceSlider.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi) * -0.5)

Note: This component orginally created by Hitendra Solanki and it was horizontally, I updated, changed some properties of it and maked it vertically.

@hitendradeveloper
Copy link

hitendradeveloper commented Apr 9, 2019

@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
Copy link
Author

@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.

Perfect πŸ‘πŸ»πŸ‘πŸ»πŸ‘πŸ»πŸ‘πŸ»

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment