Skip to content

Instantly share code, notes, and snippets.

@chanphiromsok
Forked from Dev1an/AStepIndicator.swift
Created December 11, 2023 03:50
Show Gist options
  • Save chanphiromsok/699b366686f4e726f48bb192ea0c8987 to your computer and use it in GitHub Desktop.
Save chanphiromsok/699b366686f4e726f48bb192ea0c8987 to your computer and use it in GitHub Desktop.
Simple step indicator view for iOS using UIStackView, IBDesignable, IBInspectable, Auto Layout
//
// Stepper.swift
// Stepperindicator
//
// Created by Damiaan on 13/01/2019.
// Copyright © 2019 Devian. All rights reserved.
//
import UIKit
extension UIColor {
func mixed(with color: UIColor) -> UIColor? {
guard let result = cgColor.mixed(with: color.cgColor) else {return nil}
return UIColor(cgColor: result)
}
}
@IBDesignable
class StepIndicator: UIView {
let stack = StackView<CircleView>(frame: .zero)
let label = UILabel(frame: .zero)
var labelConstraints = [NSLayoutConstraint]()
@IBInspectable
var stepCount: Int {
didSet {
if stepCount > oldValue {
for _ in oldValue..<stepCount {
stack.addArrangedSubview(createStepView())
}
} else {
if let index = highlightIndex, index>=stepCount {
highlightIndex = nil
}
for index in (stepCount..<oldValue).reversed() {
let view = stack[stackedView: index]
stack.removeArrangedSubview(view)
view.removeFromSuperview()
}
}
}
}
/// The index of the highlighted step. Nil if no step is currently highlighted
var highlightIndex: Int? {
willSet {
if let oldIndex = highlightIndex {
dim(circle: stack[stackedView: oldIndex])
labelConstraints.forEach { $0.isActive = false }
}
if let index = newValue {
let container = stack[stackedView: index]
highlight(circle: container)
labelConstraints = [
label.centerXAnchor.constraint(equalToSystemSpacingAfter: container.centerXAnchor, multiplier: 0),
label.centerYAnchor.constraint(equalToSystemSpacingBelow: container.centerYAnchor, multiplier: 0)
]
labelConstraints.forEach { $0.isActive = true }
label.text = "\(index + 1)"
label.isHidden = false
} else {
label.isHidden = true
}
}
}
/// An alias of `highlightIndex` but without the Optional type for use in Interface Builder.
/// The index of the highlighted step. `-1` if no step is currently highlighted.
@IBInspectable
var cursor: Int {
get { return highlightIndex ?? -1 }
set {
highlightIndex = stack.arrangedSubviews.indices.contains(newValue) ? newValue : nil
}
}
@IBInspectable
var highlightRadius: CGFloat = 15 {
didSet {
if let index = highlightIndex {
highlight(circle: stack[stackedView: index])
}
}
}
@IBInspectable
var defaultRadius: CGFloat = 10 {
didSet {
var indexRange = Array(stack.arrangedSubviews.indices)
if let index = highlightIndex {
indexRange.remove(at: index)
}
for index in indexRange {
dim(circle: stack[stackedView: index])
}
}
}
init(stepCount: Int) {
self.stepCount = stepCount
super.init(frame: .zero)
addSubViews()
}
override init(frame: CGRect) {
stepCount = 0
super.init(frame: frame)
addSubViews()
}
required init?(coder aDecoder: NSCoder) {
stepCount = 0
super.init(coder: aDecoder)
addSubViews()
}
private func createStepView() -> CircleView {
return CircleView(radius: defaultRadius, colors: [.purple, .red])
}
private func highlight(circle: CircleView) {
circle.radius = self.highlightRadius
}
private func dim(circle: CircleView) {
circle.radius = self.defaultRadius
}
private func addSubViews() {
let line = UIView(frame: .zero)
stack.translatesAutoresizingMaskIntoConstraints = false
line.translatesAutoresizingMaskIntoConstraints = false
stack.addSubview(line)
stack.distribution = .equalCentering
stack.alignment = .center
line.backgroundColor = UIColor.red.mixed(with: .purple)
addSubview(stack)
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 0).isActive = true
stack.trailingAnchor.constraint(equalToSystemSpacingAfter: trailingAnchor, multiplier: 0).isActive = true
stack.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 0).isActive = true
stack.bottomAnchor.constraint(equalToSystemSpacingBelow: bottomAnchor, multiplier: 0).isActive = true
line.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1).isActive = true
line.heightAnchor.constraint(equalToConstant: 3).isActive = true
line.centerYAnchor.constraint(equalToSystemSpacingBelow: stack.centerYAnchor, multiplier: 0).isActive = true
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .white
addSubview(label)
for _ in 0..<stepCount {
stack.addArrangedSubview(createStepView())
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: stack.intrinsicContentSize.width, height: highlightRadius*2)
}
}
class StackView<Element>: UIStackView {
var stackedViews: [Element] {
return arrangedSubviews.map {$0 as! Element}
}
subscript (stackedView index: Int) -> Element {
return arrangedSubviews[index] as! Element
}
}
//
// Interpolation.swift
// Stepperindicator
//
// Created by Damiaan on 13/01/2019.
// Copyright © 2019 Devian. All rights reserved.
//
import CoreGraphics
import simd
extension CGColor {
static let halfVector = simd_double4(0.5)
func mixed(with color: CGColor) -> CGColor? {
guard let leftSpace = colorSpace, let rightSpace = color.colorSpace, leftSpace == rightSpace, let leftComponents = components, let rightComponents = color.components else {
return nil
}
let leftVector = simd_double4(leftComponents.map{Double($0)})
let rightVector = simd_double4(rightComponents.map{Double($0)})
let resultVector = simd_mix(leftVector, rightVector, CGColor.halfVector).map {CGFloat($0)}
return CGColor(colorSpace: leftSpace, components: resultVector)
}
}
//
// Circle.swift
// Stepperindicator
//
// Created by Damiaan on 13/01/2019.
// Copyright © 2019 Devian. All rights reserved.
//
import UIKit
@IBDesignable
class CircleView: UIView {
override open class var layerClass: AnyClass {
return CAGradientLayer.classForCoder()
}
static let radiusCodingKey = "com.dev1an.CircleView.radius"
public var radius: CGFloat {
didSet {
invalidateIntrinsicContentSize()
layer.cornerRadius = radius
superview?.setNeedsLayout()
superview?.layoutIfNeeded()
}
}
init(radius: CGFloat, colors: [UIColor]) {
self.radius = radius
let diameter = radius * 2
let containingSquare = CGRect(x: 0, y: 0, width: diameter, height: diameter)
super.init(frame: containingSquare)
makeCircle(colors: colors)
}
override init(frame: CGRect) {
print("init circle with frame")
radius = min(frame.size.height, frame.size.width) / 2
super.init(frame: frame)
makeCircle(colors: [.darkText])
}
required init?(coder aDecoder: NSCoder) {
radius = 0
super.init(coder: aDecoder)
radius = min(frame.size.height, frame.size.width) / 2
makeCircle(colors: [.blue, .purple])
}
func makeCircle(colors: [UIColor]) {
layer.cornerRadius = radius
setContentHuggingPriority(.required, for: .horizontal)
setContentHuggingPriority(.required, for: .vertical)
(layer as! CAGradientLayer).colors = colors.map {$0.cgColor}
}
override var intrinsicContentSize: CGSize {
let diameter = radius * 2
return CGSize(width: diameter, height: diameter)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment