-
-
Save chanphiromsok/699b366686f4e726f48bb192ea0c8987 to your computer and use it in GitHub Desktop.
Simple step indicator view for iOS using UIStackView, IBDesignable, IBInspectable, Auto Layout
This file contains 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
// | |
// 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 | |
} | |
} |
This file contains 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
// | |
// 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) | |
} | |
} |
This file contains 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
// | |
// 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