Created
March 23, 2022 19:42
-
-
Save DonMag/45092f7c305968650da5896bbe045873 to your computer and use it in GitHub Desktop.
Three approaches to multi-color "sectioned" view
This file contains hidden or 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
// 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