Skip to content

Instantly share code, notes, and snippets.

@devshok
Last active July 11, 2022 21:32
Show Gist options
  • Save devshok/169e20dd0b8d45be6d2cfd9aa36b2e23 to your computer and use it in GitHub Desktop.
Save devshok/169e20dd0b8d45be6d2cfd9aa36b2e23 to your computer and use it in GitHub Desktop.
// Created by Ivan Shokurov on 8/12/20.
// This a custom slider (by UISlider)
// with two thumbs instead of default one.
import UIKit
//swiftlint:disable:next type_body_length
public final class DoubledSlider: UIControl, ViewProgrammatically {
// MARK: - UI
private lazy var track = UIView()
private lazy var activeTrack = UIView()
private lazy var leftThumb = UIImageView()
private lazy var rightThumb = UIImageView()
// MARK: - Initialization
public convenience init(minimumValue: Float = .zero, maximumValue: Float = .zero) {
self.init()
self.minimumValue = minimumValue
self.maximumValue = maximumValue
}
private override init(frame: CGRect = .zero) {
super.init(frame: frame)
self.addSubviews()
self.setupSubviews()
self.makeConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Public API
public var minimumValue: Float = .zero
public var maximumValue: Float = .zero
public var values: (minimum: Float, maximum: Float) {
get { return (self.minimumValueNow, self.maximumValueNow) }
set {
var newMin: Float = .zero; var newMax: Float = .zero
if newValue.0 <= .zero {
newMin = self.minimumValue
}
if newValue.0 <= self.minimumValue {
newMin = self.minimumValue
}
if newValue.0 > newValue.1 {
newMin = newValue.1
}
if newValue.1 <= .zero {
newMax = self.minimumValue
}
if newValue.1 >= self.maximumValue {
newMax = self.maximumValue
}
if newValue.1 < newValue.0 {
newMax = newValue.0
}
self.minimumValueNow = newMin
self.maximumValueNow = newMax
}
}
// MARK: - Private API
private var minimumValueNow: Float = .zero
private var maximumValueNow: Float = .zero
private func updateValues(_ completion: @escaping () -> Void) {
self.minimumValueNow = self.newMinimalValue
self.maximumValueNow = self.newMaximumValue
let originXDifference = abs(self.rightThumb.frame.origin.x - self.leftThumb.frame.origin.x)
if originXDifference < 20 {
self.minimumValueNow = self.maximumValueNow
}
completion()
}
private var newMinimalValue: Float {
let leftThumbOriginX = self.leftThumbConstraint.constant
let halfOfThumbWidth = self.leftThumb.frame.size.width / 2
let distancePassed = leftThumbOriginX + halfOfThumbWidth
let totalDistance = self.track.frame.size.width
let distancePassedFraction = distancePassed / totalDistance
if leftThumbOriginX < 3 {
return self.minimumValue
}
if totalDistance - (leftThumbOriginX + self.leftThumb.frame.size.width) < 3 {
return self.maximumValue
}
let newValue: Float = {
let difference = self.maximumValue - self.minimumValue
let addingValue = Float(distancePassedFraction) * difference
return self.minimumValue + addingValue
}()
return newValue
}
private var newMaximumValue: Float {
let constraintConstant = abs(self.rightThumbConstraint.constant)
let distancePassed = constraintConstant + self.leftThumb.frame.size.width / 2
let totalDistance = self.track.frame.size.width
let distancePassedFraction = distancePassed / totalDistance
if constraintConstant < 3 {
return self.maximumValue
}
if totalDistance - (self.rightThumb.frame.size.width + constraintConstant) < 3 {
return self.minimumValue
}
let newValue: Float = {
let difference = self.maximumValue - self.minimumValue
let addingValue = difference * Float(distancePassedFraction)
return self.maximumValue - addingValue
}()
return newValue
}
// MARK: - View Programmatically
public func addSubviews() {
self.addSubview(self.track)
self.track.addSubview(self.activeTrack)
self.addSubview(self.leftThumb)
self.addSubview(self.rightThumb)
}
public func setupSubviews() {
self.setupItself()
self.setupTrack()
self.setupActiveTrack()
self.setupThumbs()
}
public func makeConstraints() {
defer {
[self.leftThumb, self.rightThumb].forEach { self.bringSubviewToFront($0) }
}
self.makeConstraintsForTrack()
self.makeConstraintsForThumbs()
self.makeConstraintsForActiveTrack()
}
// MARK: - Setups
private func setupItself() {
self.backgroundColor = .clear
self.translatesAutoresizingMaskIntoConstraints = false
}
private func setupTrack() {
self.track.layer.masksToBounds = true
self.track.layer.cornerRadius = 1.5
self.track.backgroundColor = A.superLightGrayBackground.color
self.track.clipsToBounds = true
}
private func setupActiveTrack() {
self.activeTrack.backgroundColor = A.purpleishBlue.color
}
private func setupThumbs() {
[self.leftThumb, self.rightThumb].forEach {
$0.image = A.sliderThumb.image
$0.contentMode = .scaleAspectFit
$0.clipsToBounds = true
$0.isUserInteractionEnabled = true
let gesture = UIPanGestureRecognizer(target: self, action: #selector(self.dragThumb(using:)))
$0.addGestureRecognizer(gesture)
}
self.leftThumb.tag = 1
self.rightThumb.tag = 2
}
@objc private func dragThumb(using gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else { return }
let translation = gesture.translation(in: self)
switch gesture.state {
case .began:
if [1, 2].contains(view.tag) { self.bringSubviewToFront(view) }
case .changed:
self.handleDragging(view, withTranslation: translation)
case .ended:
self.handleEndedDragging(view)
default:
break
}
}
private func handleDragging(_ view: UIView, withTranslation translation: CGPoint) {
switch view.tag {
case 1:
self.handleDraggingLeftThumb(view, withTranslation: translation)
case 2:
self.handleDraggingRightThumb(view, withTranslation: translation)
default:
break
}
}
private func handleDraggingLeftThumb(_ thumb: UIView, withTranslation translation: CGPoint) {
let newConstant = translation.x + self.leftThumbConstraintLastConstant
guard newConstant > -0.001 else {
self.leftThumbConstraint.constant = .zero
self.leftActiveTrackConstraint.constant = .zero
FeedbackManager.shared.vibrate()
self.updateValues {
self.sendActions(for: .valueChanged)
}
return
}
if self.thumbesInOnePlace(byDraggingThumb: thumb) && translation.x > .zero { return }
self.leftThumbConstraint.constant = newConstant
self.leftActiveTrackConstraint.constant = newConstant
self.updateValues {
self.sendActions(for: .valueChanged)
}
}
private func handleDraggingRightThumb(_ thumb: UIView, withTranslation translation: CGPoint) {
let newConstant = translation.x + self.rightThumbConstraintLastConstant
guard newConstant < 0.001 else {
self.rightThumbConstraint.constant = .zero
self.rightActiveTrackConstraint.constant = .zero
FeedbackManager.shared.vibrate()
self.updateValues {
self.sendActions(for: .valueChanged)
}
return
}
if self.thumbesInOnePlace(byDraggingThumb: thumb) && translation.x < .zero { return }
self.rightThumbConstraint.constant = newConstant
self.rightActiveTrackConstraint.constant = newConstant
self.updateValues {
self.sendActions(for: .valueChanged)
}
}
private func handleEndedDragging(_ view: UIView) {
switch view.tag {
case 1:
self.leftThumbConstraintLastConstant = self.leftThumbConstraint.constant
self.leftActiveTrackConstraintLastConstant = self.leftActiveTrackConstraint.constant
case 2:
self.rightThumbConstraintLastConstant = self.rightThumbConstraint.constant
self.rightActiveTrackConstraintLastConstant = self.rightActiveTrackConstraint.constant
default:
break
}
}
private func thumbesInOnePlace(byDraggingThumb thumb: UIView) -> Bool {
let difference = abs(self.leftThumb.center.x - self.rightThumb.center.x)
let onePlace = difference < 10
if onePlace {
self.forceThumbsPositions(byActiveThumb: thumb)
}
return onePlace
}
private func forceThumbsPositions(byActiveThumb thumb: UIView) {
defer {
if [1, 2].contains(thumb.tag) { FeedbackManager.shared.vibrate() }
}
switch thumb.tag {
case 1:
let newConstant = self.rightThumb.frame.origin.x
self.leftThumbConstraint.constant = newConstant
self.leftThumbConstraintLastConstant = newConstant
self.leftActiveTrackConstraint.constant = newConstant
self.leftActiveTrackConstraintLastConstant = newConstant
case 2:
let centerXDifference = self.rightThumb.center.x - self.leftThumb.center.x
let newConstant = self.rightThumbConstraint.constant - centerXDifference
self.rightThumbConstraint.constant = newConstant
self.rightThumbConstraintLastConstant = newConstant
self.rightActiveTrackConstraint.constant = newConstant
self.rightActiveTrackConstraintLastConstant = newConstant
default:
break
}
}
// MARK: - Constraints
private func makeConstraintsForTrack() {
self.track.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.track.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.track.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.track.heightAnchor.constraint(equalToConstant: 3),
self.track.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
}
private func makeConstraintsForActiveTrack() {
self.activeTrack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.leftActiveTrackConstraint,
self.rightActiveTrackConstraint,
self.activeTrack.topAnchor.constraint(equalTo: self.track.topAnchor),
self.activeTrack.bottomAnchor.constraint(equalTo: self.track.bottomAnchor)
])
}
private func makeConstraintsForThumbs() {
[self.leftThumb, self.rightThumb].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.widthAnchor.constraint(equalToConstant: 26).isActive = true
$0.heightAnchor.constraint(equalToConstant: 26).isActive = true
$0.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
$0.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
self.leftThumbConstraint.isActive = true
self.rightThumbConstraint.isActive = true
}
private lazy var leftThumbConstraint: NSLayoutConstraint = {
return self.leftThumb.leadingAnchor.constraint(greaterThanOrEqualTo: self.track.leadingAnchor)
}()
private lazy var rightThumbConstraint: NSLayoutConstraint = {
return self.rightThumb.trailingAnchor.constraint(lessThanOrEqualTo: self.track.trailingAnchor)
}()
private var leftThumbConstraintLastConstant: CGFloat = .zero
private var rightThumbConstraintLastConstant: CGFloat = .zero
private lazy var leftActiveTrackConstraint: NSLayoutConstraint = {
return self.activeTrack.leftAnchor.constraint(equalTo: self.track.leftAnchor)
}()
private lazy var rightActiveTrackConstraint: NSLayoutConstraint = {
return self.activeTrack.rightAnchor.constraint(equalTo: self.track.rightAnchor)
}()
private var leftActiveTrackConstraintLastConstant: CGFloat = .zero
private var rightActiveTrackConstraintLastConstant: CGFloat = .zero
}
public protocol ViewProgrammatically {
func addSubviews()
func setupSubviews()
func makeConstraints()
}
@chaitanyasai-pcs
Copy link

Hi,

How can I implement this in uitableviewcell ? Can you please give me some insights ?

@devshok
Copy link
Author

devshok commented Nov 23, 2021

@chaitanyasai-pcs hi! If you want to add my or any other custom views programmatically into the cell, first of all, you should make a subclass of UITableViewCell and then in initialization of instances of your subclass (let's say, it's SliderTableViewCell class) you should implement adding and setting up your custom view adding this to your cell when it will initialize itself.
One more time:

  1. Subclass UITableViewCell
  2. Implement initialization for your custom cell subclass, adding any other custom subview like mine for example
  3. Add and conform your cell subclass in table view using UITableViewDataSource.

For example, how to add any custom views into parent view, you can check here. But of course, you can make research using Google and find a lot of ways how to deal with that.

Hope, it helps you anyhow. Thanks for interesting. Ask me anything about this if you'd like.

@woodyjhonson
Copy link

Hi! Please, could you help me, how can I set current value of thumbs ?

@devshok
Copy link
Author

devshok commented Jul 11, 2022

@stepanovgeorgii

Hi! Please, could you help me, how can I set current value of thumbs ?

Hey there!
Hope it's not too much complicated to find out how it works.

There's var values in public API you can use for setting up current values that can be between min and max values you initialize when calling this slider in your program. If your new current values are gonna be out of range, for example, minimum value will be less than minimum value of the left thumb, well, computed public property called values doesn't change anything but no any logic crash.

At least, I tested it.

Hope, you find my response well.

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