Skip to content

Instantly share code, notes, and snippets.

@DonMag
Created March 23, 2022 19:42
Show Gist options
  • Save DonMag/45092f7c305968650da5896bbe045873 to your computer and use it in GitHub Desktop.
Save DonMag/45092f7c305968650da5896bbe045873 to your computer and use it in GitHub Desktop.
Three approaches to multi-color "sectioned" view
// three different ways (among many) to implement a multi-colored progress- or slider-style view.
struct RecordingStep {
var color: UIColor = .black
var start: Float = 0
var end: Float = 0
// layer is used only with MultiLayerStepView
var layer: CALayer!
}
// "base" view... common elements and functions
// the other three view classes are all subclassed from BaseStepView
class BaseStepView: UIView {
public var progress: Float = 0 {
didSet {
// move the progress layer
progressLayer.position.x = bounds.width * CGFloat(progress)
// if we're recording
if isRecording {
let i = theSteps.count - 1
guard i > -1 else { return }
// update current "step" end
theSteps[i].end = progress
setNeedsLayout()
setNeedsDisplay()
}
}
}
internal var isRecording: Bool = false
internal var theSteps: [RecordingStep] = []
internal let progressLayer = CAShapeLayer()
public func startRecording(_ color: UIColor) {
// always handled by subclass
}
public func stopRecording() {
isRecording = false
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
backgroundColor = .black
progressLayer.lineWidth = 3
progressLayer.strokeColor = UIColor.green.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(progressLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// only set the progessLayer frame if the bounds height has changed
if progressLayer.frame.height != bounds.height + 7.0 {
let r: CGRect = CGRect(origin: .zero, size: CGSize(width: 7.0, height: bounds.height + 7.0))
let pth = UIBezierPath(roundedRect: r, cornerRadius: 3.5)
progressLayer.frame = r
progressLayer.position = CGPoint(x: 0, y: bounds.midY)
progressLayer.path = pth.cgPath
}
}
}
// using a CAGradientLayer
class GradStepView: BaseStepView {
private let gLayer = CAGradientLayer()
override func commonInit() {
layer.addSublayer(gLayer)
gLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
super.commonInit()
}
override func startRecording(_ color: UIColor) {
super.startRecording(color)
// create a new "Recording Step"
var st = RecordingStep()
st.color = color
st.start = progress
st.end = progress
theSteps.append(st)
isRecording = true
}
override func layoutSubviews() {
super.layoutSubviews()
gLayer.frame = bounds
var c: [CGColor] = []
var l: [NSNumber] = []
var curLoc: Float = 0.0
theSteps.forEach { st in
let x1 = st.start
let x2 = st.end
if x1 != curLoc {
// we need a clear section
c.append(UIColor.clear.cgColor)
c.append(UIColor.clear.cgColor)
l.append(NSNumber(value: curLoc))
l.append(NSNumber(value: x1))
}
c.append(st.color.cgColor)
c.append(st.color.cgColor)
l.append(NSNumber(value: x1))
l.append(NSNumber(value: x2))
curLoc = x2
}
if curLoc < 1.0 {
// if the last step does not exxtend to 1.0
// add another clear section
c.append(UIColor.clear.cgColor)
c.append(UIColor.clear.cgColor)
l.append(NSNumber(value: curLoc))
l.append(NSNumber(value: 1.0))
}
gLayer.colors = c
gLayer.locations = l
}
}
// overriding draw()
class DrawStepView: BaseStepView {
override func startRecording(_ color: UIColor) {
super.startRecording(color)
// create a new "Recording Step"
var st = RecordingStep()
st.color = color
st.start = progress
st.end = progress
theSteps.append(st)
isRecording = true
}
override func draw(_ rect: CGRect) {
super.draw(rect)
theSteps.forEach { st in
let x = bounds.width * CGFloat(st.start)
let w = bounds.width * CGFloat(st.end - st.start)
let r = CGRect(x: x, y: 0.0, width: w, height: bounds.height)
st.color.setFill()
let pth = UIBezierPath(rect: r)
pth.fill()
}
}
}
// using multiple layers - one for each "step"
class MultiLayerStepView: BaseStepView {
override func startRecording(_ color: UIColor) {
// create a new "Recording Step"
var st = RecordingStep()
st.color = color
st.start = progress
st.end = progress
let l = CALayer()
l.backgroundColor = st.color.cgColor
layer.insertSublayer(l, below: progressLayer)
st.layer = l
theSteps.append(st)
isRecording = true
}
override func layoutSubviews() {
super.layoutSubviews()
theSteps.forEach { st in
let x = bounds.width * CGFloat(st.start)
let w = bounds.width * CGFloat(st.end - st.start)
let r = CGRect(x: x, y: 0.0, width: w, height: bounds.height)
st.layer.frame = r
}
}
}
// -------------------------
// example view controller using all 3 approaches
class ExampleVC: UIViewController {
let mlStepView = MultiLayerStepView()
let gradStepView = GradStepView()
let drawStepView = DrawStepView()
let actionButton: UIButton = {
let b = UIButton()
b.backgroundColor = .lightGray
b.setImage(UIImage(systemName: "play.fill"), for: [])
b.tintColor = .systemGreen
return b
}()
var timer: Timer!
let colors: [UIColor] = [
.red, .systemBlue, .yellow, .cyan, .magenta, .orange,
]
var colorIdx: Int = -1
var action: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
[mlStepView, gradStepView, drawStepView, actionButton].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mlStepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
mlStepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
mlStepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
mlStepView.heightAnchor.constraint(equalToConstant: 40.0),
gradStepView.topAnchor.constraint(equalTo: mlStepView.bottomAnchor, constant: 40.0),
gradStepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
gradStepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
gradStepView.heightAnchor.constraint(equalToConstant: 40.0),
drawStepView.topAnchor.constraint(equalTo: gradStepView.bottomAnchor, constant: 40.0),
drawStepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
drawStepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
drawStepView.heightAnchor.constraint(equalToConstant: 40.0),
actionButton.topAnchor.constraint(equalTo: drawStepView.bottomAnchor, constant: 40.0),
actionButton.widthAnchor.constraint(equalToConstant: 80.0),
actionButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
actionButton.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
}
@objc func timerFunc(_ timer: Timer) {
// don't set progress > 1.0
[mlStepView, gradStepView, drawStepView].forEach { v in
v.progress = min(v.progress + 0.005, 1.0)
}
if mlStepView.progress >= 1.0 {
timer.invalidate()
actionButton.isHidden = true
[mlStepView, gradStepView, drawStepView].forEach { v in
v.stopRecording()
}
}
}
@objc func btnTap(_ sender: UIButton) {
switch action {
case 0:
// this will run for 15 seconds
timer = Timer.scheduledTimer(timeInterval: 0.075, target: self, selector: #selector(timerFunc(_:)), userInfo: nil, repeats: true)
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
case 1:
colorIdx += 1
[mlStepView, gradStepView, drawStepView].forEach { v in
v.startRecording(colors[colorIdx % colors.count])
}
actionButton.setImage(UIImage(systemName: "stop.circle"), for: [])
actionButton.tintColor = .black
action = 2
case 2:
[mlStepView, gradStepView, drawStepView].forEach { v in
v.stopRecording()
}
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
default:
()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment