Skip to content

Instantly share code, notes, and snippets.

@mazen-kasser
Last active June 30, 2022 09:41
Show Gist options
  • Save mazen-kasser/e133ff3797d9e2393eb604a57941c789 to your computer and use it in GitHub Desktop.
Save mazen-kasser/e133ff3797d9e2393eb604a57941c789 to your computer and use it in GitHub Desktop.
Stepper component based on `UIStepper` control with having a label as the divider image.
import UIKit
/// Stepper component based on `UIStepper` control.
///
/// - Parameters:
/// - onClick, closure returns the final `value`
/// - isContinuous, default = YES. value change events are sent any time the value changes during interaction.
/// - autorepeat, default = YES. press & hold repeatedly alters value.
/// - wraps, default = NO. value wraps from min <-> max.
/// - value, default = 0. sends UIControlEventValueChanged. clamped to min/max
/// - minimumValue, default 0. must be less than maximumValue
/// - maximumValue, default 100. must be greater than minimumValue
/// - stepValue, default 1. must be greater than 0
class StepperControl: UIStepper {
var onClick: ((Double) -> Void)?
private lazy var label: UILabel! = {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: frame.height))
label.textAlignment = .center
label.textColor = .black
label.adjustsFontSizeToFitWidth = true
return label
}()
private
enum Constants {
static let decrementImage = UIImage(systemName: "minus")!
static let incrementImage = UIImage(systemName: "plus")!
}
override var value: Double {
willSet {
label.text = NSNumber(value: value).stringValue
configureUI()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
configureUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
configureUI()
}
override func awakeFromNib() {
super.awakeFromNib()
setupUI()
configureUI()
}
private func setupUI() {
// set events
addTarget(self, action: #selector(valueChange), for: .valueChanged)
addTarget(self, action: #selector(valueChanged), for: .touchUpInside)
let normalColor = UIColor.systemGreen
let highlightedColor = UIColor.green
let disabledColor = UIColor.gray
// adjust images size
let font = UIFont.systemFont(ofSize: 32)
label.font = font
let configuration = UIImage.SymbolConfiguration(font: font)
let decrementImage = Constants.decrementImage
.withConfiguration(configuration)
.withRenderingMode(.alwaysOriginal)
let incrementImage = Constants.incrementImage
.withConfiguration(configuration)
.withRenderingMode(.alwaysOriginal)
// set style
setDecrementImage(decrementImage.withTintColor(normalColor), for: .normal)
setDecrementImage(decrementImage.withTintColor(highlightedColor), for: .highlighted)
setDecrementImage(decrementImage.withTintColor(disabledColor), for: .disabled)
setIncrementImage(incrementImage.withTintColor(normalColor), for: .normal)
setIncrementImage(incrementImage.withTintColor(highlightedColor), for: .highlighted)
setIncrementImage(incrementImage.withTintColor(disabledColor), for: .disabled)
let backgroundBlankImage = UIGraphicsImageRenderer(size: CGSize(width: 64, height: 8)).image(actions: {_ in })
setBackgroundImage(backgroundBlankImage, for: .normal)
}
private func configureUI() {
// set divider image for normal / normal
setDividerImage(image(fromView: label),
forLeftSegmentState: .normal,
rightSegmentState: .normal)
// set divider image for normal / highlighted
setDividerImage(image(fromView: label),
forLeftSegmentState: .normal,
rightSegmentState: .highlighted)
// set divider image for highlighted / normal
setDividerImage(image(fromView: label),
forLeftSegmentState: .highlighted,
rightSegmentState: .normal)
}
@objc
private func valueChange(sender: UIStepper) {
value = sender.value // to update the divider label
}
@objc private func valueChanged(sender: UIStepper) {
onClick?(sender.value) // to return the final updated value after long press or short press
}
private func image(fromView view: UIView) -> UIImage {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
view.layer.render(in: UIGraphicsGetCurrentContext()!)
return UIGraphicsGetImageFromCurrentImageContext()!
}
}
@mazen-kasser
Copy link
Author

mazen-kasser commented Jun 30, 2022

To consume it is just as simple as setting the properties:

let stepper = StepperControl(frame: CGRect(x: 100, y: 100, width: 0, height: 0))
stepper.value = 5  // must be set first to render the initial value
stepper.maximumValue = 36
stepper.minimumValue = 1
view.addSubview(stepper)

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