CAShapeLayer with different Colors - ios

I have a CAShapeLayer based on this answer that animates along with a UISlider.
It works fine but as the shapeLayer follows along its just 1 red CAGradientLayer color. What I want is the shapeLayer to change colors based on certain points of the slider. An example is at 0.4 - 0.5 it's red, 0.7-0.8 red, 0.9-0.95 red. Those aren't actual values, the actual values will vary. I figure that any time it doesn't meet the condition to turn red it should probably just be a clear color, which will just show the black track underneath it. The result would look something like this (never mind the shape)
The red colors are based on the user scrubbing the slider and the letting go. The different positions of the slider that determine the red color is based on whatever condition. How can I do this.
UISlider
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.minimumTrackTintColor = .blue
s.maximumTrackTintColor = .white
s.minimumValue = 0
s.maximumValue = 1
s.addTarget(self, action: #selector(onSliderChange), for: .valueChanged)
return s
s.addTarget(self, action: #selector(onSliderEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
return s
}()
lazy var progressView: GradientProgressView = {
let v = GradientProgressView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
#objc fileprivate func onSliderChange(_ slider: UISlider) {
let condition: Bool = // ...
let value = slider.value
progressView.setProgress(CGFloat(value), someCondition: condition, slider_X_Position: slider_X_PositionInView())
}
#objc fileprivate func onSliderEnded(_ slider: UISlider) {
let value = slider.value
progressView.resetProgress(CGFloat(value))
}
// ... progressView is the same width as the the slider
func slider_X_PositionInView() -> CGFloat {
let trackRect = slider.trackRect(forBounds: slider.bounds)
let thumbRect = slider.thumbRect(forBounds: slider.bounds,
trackRect: trackRect,
value: slider.value)
let convertedThumbRect = slider.convert(thumbRect, to: self.view)
return convertedThumbRect.midX
}
GradientProgressView:
public class GradientProgressView: UIView {
var shapeLayer: CAShapeLayer = {
// ...
}()
private var trackLayer: CAShapeLayer = {
let trackLayer = CAShapeLayer()
trackLayer.strokeColor = UIColor.black.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
return trackLayer
}()
private var gradient: CAGradientLayer = {
let gradient = CAGradientLayer()
let redColor = UIColor.red.cgColor
gradient.colors = [redColor, redColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
return gradient
}()
// ... add the above layers as subLayers to self ...
func updatePaths() { // added in layoutSubviews
let lineWidth = bounds.height / 2
trackLayer.lineWidth = lineWidth * 0.75
shapeLayer.lineWidth = lineWidth
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))
trackLayer.path = path.cgPath
shapeLayer.path = path.cgPath
gradient.frame = bounds
gradient.mask = shapeLayer
shapeLayer.duration = 1
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
}
public func setProgress(_ progress: CGFloat, someCondition: Bool, slider_X_Position: CGFloat) {
// slider_X_Position might help with shapeLayer's x position for the colors ???
if someCondition {
// redColor until the user lets go
} else {
// otherwise always a clearColor
}
shapeLayer.strokeEnd = progress
}
}
public func resetProgress(_ progress: CGFloat) {
// change to clearColor after finger is lifted
}
}

To get this:
We can use a CAShapeLayer for the red "boxes" and a CALayer as a .mask on that shape layer.
To reveal / cover the boxes, we set the frame of the mask layer to a percentage of the width of the bounds.
Here's a complete example:
class StepView: UIView {
public var progress: CGFloat = 0 {
didSet {
setNeedsLayout()
}
}
public var steps: [[CGFloat]] = [[0.0, 1.0]] {
didSet {
setNeedsLayout()
}
}
public var color: UIColor = .red {
didSet {
stepLayer.fillColor = color.cgColor
}
}
private let stepLayer = CAShapeLayer()
private let maskLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
layer.addSublayer(stepLayer)
stepLayer.fillColor = color.cgColor
stepLayer.mask = maskLayer
// mask layer can use any solid color
maskLayer.backgroundColor = UIColor.white.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
stepLayer.frame = bounds
let pth = UIBezierPath()
steps.forEach { pair in
// rectangle for each "percentage pair"
let w = bounds.width * (pair[1] - pair[0])
let b = UIBezierPath(rect: CGRect(x: bounds.width * pair[0], y: 0, width: w, height: bounds.height))
pth.append(b)
}
stepLayer.path = pth.cgPath
// update frame of mask layer
var r = bounds
r.size.width = bounds.width * progress
maskLayer.frame = r
}
}
class StepVC: UIViewController {
let stepView = StepView()
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(slider)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
slider.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let steps: [[CGFloat]] = [
[0.1, 0.3],
[0.4, 0.5],
[0.7, 0.8],
[0.9, 0.95],
]
stepView.steps = steps
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
}
#objc func sliderChanged(_ sender: UISlider) {
// disable CALayer "built-in" animations
CATransaction.setDisableActions(true)
stepView.progress = CGFloat(sender.value)
CATransaction.commit()
}
}
Edit
I'm still not clear on your 0.4 - 0.8 requirement, but maybe this will help get you on your way:
Please note: this is Example Code Only!!!
struct RecordingStep {
var color: UIColor = .black
var start: Float = 0
var end: Float = 0
var layer: CALayer!
}
class StepView2: 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()
}
}
}
private var isRecording: Bool = false
private var theSteps: [RecordingStep] = []
private let progressLayer = CAShapeLayer()
public 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
}
public func stopRecording() {
isRecording = false
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private 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
}
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
}
}
}
class Step2VC: UIViewController {
let stepView = StepView2()
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()
stepView.translatesAutoresizingMaskIntoConstraints = false
actionButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(actionButton)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
actionButton.topAnchor.constraint(equalTo: stepView.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
stepView.progress = min(stepView.progress + 0.005, 1.0)
if stepView.progress >= 1.0 {
timer.invalidate()
actionButton.isHidden = true
}
}
#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)
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
case 1:
colorIdx += 1
stepView.startRecording(colors[colorIdx % colors.count])
actionButton.setImage(UIImage(systemName: "stop.circle"), for: [])
actionButton.tintColor = .black
action = 2
case 2:
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
default:
()
}
}
}
For future reference, when posting here, it's probably a good idea to fully explain what you're trying to do. Showing code you're working on is important, but if it's really only sorta related to your actual goal, it makes this process pretty difficult.

Related

Animation not happening as it is supposed to be for subclass of UIView with layers

I am trying to animate a subclass of UIView that has a some layers with shadow. In a view controller, I set this view inside a container. I animate container's height constraint. The container (the purple one in the video) animates properly, but the view that is supposed to be animated, doesn't animate the way it should be.
How it looks now
This is how I animate the container view.
func updateWhiteCircle(with progressHeight: CGFloat?) {
guard let progressHeight = progressHeight else {
return
}
neumorphicRingProgressHeightConstraint.constant = progressHeight
UIView.animate(withDuration: 1.0) { [weak self] in
self?.view.layoutIfNeeded()
}
}
Where do I get it wrong? Why doesn't it animate the way it should be?
You didn't show us how you're generating the "white shadow," but likely you're setting it in the view's layoutSubviews() func.
The problem is, that does not animate with the size of the view.
You probably want to animate the path for the "shadow layer."
Here's a quick example:
class WhiteCircleView: UIView {
private let shapeLayer = CAShapeLayer()
// public vars so we can set various properties
public var fillColor: UIColor = .white {
didSet {
shapeLayer.fillColor = fillColor.cgColor
}
}
public var shadowColor: UIColor = .white {
didSet {
shapeLayer.shadowColor = shadowColor.cgColor
}
}
public var shadowOpacity: Float = 1.0 {
didSet {
shapeLayer.shadowOpacity = shadowOpacity
}
}
public var shadowRadius: CGFloat = 20 {
didSet {
shapeLayer.shadowRadius = shadowRadius
}
}
public var shadowOffset: CGSize = .zero {
didSet {
shapeLayer.shadowOffset = shadowOffset
}
}
public var progress: CGFloat = 0 {
didSet {
animCircle()
}
}
private var curProgress: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.shadowColor = shadowColor.cgColor
shapeLayer.shadowOffset = shadowOffset
shapeLayer.shadowRadius = shadowRadius
shapeLayer.shadowOpacity = shadowOpacity
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
let w: CGFloat = bounds.width * CGFloat(progress)
let wi: CGFloat = (bounds.width - w) * 0.5
let newPath = UIBezierPath(ovalIn: bounds.insetBy(dx: wi, dy: wi)).cgPath
shapeLayer.path = newPath
}
private func animCircle() {
print(progress)
let w: CGFloat = bounds.width * CGFloat(progress)
let wi: CGFloat = (bounds.width - w) * 0.5
let curPath = shapeLayer.path
let newPath = UIBezierPath(ovalIn: bounds.insetBy(dx: wi, dy: wi)).cgPath
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = curPath
animation.toValue = newPath
animation.duration = 0.5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
CATransaction.setCompletionBlock({
// update to new path on anim end
self.shapeLayer.path = newPath
})
shapeLayer.add(animation, forKey: "grow")
CATransaction.commit()
}
}
class AnimCircleVC: UIViewController {
let wcv = WhiteCircleView()
let bkgColor: UIColor = UIColor(white: 0.75, alpha: 1.0)
var progress: CGFloat = 0
// a "center label" to show progress value
let pLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .systemYellow
v.textAlignment = .center
v.text = "0%"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = bkgColor
wcv.fillColor = bkgColor
wcv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wcv)
pLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wcv.centerXAnchor.constraint(equalTo: g.centerXAnchor),
wcv.centerYAnchor.constraint(equalTo: g.topAnchor, constant: 200.0),
wcv.heightAnchor.constraint(equalToConstant: 200.0),
wcv.widthAnchor.constraint(equalTo: wcv.heightAnchor),
pLabel.centerXAnchor.constraint(equalTo: wcv.centerXAnchor),
pLabel.centerYAnchor.constraint(equalTo: wcv.centerYAnchor),
pLabel.widthAnchor.constraint(equalToConstant: 100.0),
pLabel.heightAnchor.constraint(equalTo: pLabel.widthAnchor),
])
pLabel.layer.cornerRadius = 50.0
pLabel.layer.masksToBounds = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if progress >= 1.0 {
// shrink it away
progress = 0.0
} else {
if progress == 0.0 {
// start at 60%
progress = 0.6
} else {
// increment by 10%
progress += 0.10
}
}
// floating point can result in 0.99999...
// so round to 1/100th
progress = (progress * 100).rounded() / 100.0
wcv.progress = progress
pLabel.text = "\(Int(progress * 100))%"
}
}

Initializing an instance from Custom ProgressBar Class into UItableviewCell

I'm trying to create a circular progressBar with timer function in my UItableviewCell class. I have created a custom class timer, and I am using Jonni's progressBar template (Credit to
Jonni Ã…kesson) from - https://github.com/innoj/CountdownTimer.
The only noticeable difference between my code and source template is that I am adding the Custom ProgressBar class UIView programmatically whereas the source template used IBOutlet to connect to the Custom ProgressBar Class.
My goal is to ensure that the CAShapeLayers (both actual and background layers are shown as per below image - Source: https://www.youtube.com/watch?v=-KwFvGVstyc
Below is my code and I am unable to see both front CAShapeLayer and background CAShapeLayer. I am suspecting a logical error when I initialize constant "progressBar".
Please advise if it is possible to utilize the Custom ProgressBar Class, instead of re-writing the entire code.
import Foundation
import UIKit
import IQKeyboardManagerSwift
class ActiveExerciseTableViewCell: UITableViewCell, UITextFieldDelegate {
let progressBar: ProgressBar = {
let progressBar = ProgressBar()
return progressBar
}()
lazy var activeExerciseTimerUIView: UIView = { [weak self] in
let activeExerciseTimerUIView = UIView()
activeExerciseTimerUIView.backgroundColor = .black
activeExerciseTimerUIView.isUserInteractionEnabled = true
activeExerciseTimerUIView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapTimerView)))
return activeExerciseTimerUIView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(activeExerciseTimerUIView)
}
override func layoutSubviews() {
super.layoutSubviews()
setUpActiveExerciseUIViewLayout()
}
func setUpActiveExerciseUIViewLayout(){
activeExerciseTimerUIView.translatesAutoresizingMaskIntoConstraints = false
activeExerciseTimerUIView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
activeExerciseTimerUIView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentView.frame.height-tableviewContentViewTabBarHeight)*0.25).isActive = true
activeExerciseTimerUIView.widthAnchor.constraint(equalToConstant: 225).isActive = true
activeExerciseTimerUIView.heightAnchor.constraint(equalToConstant: 225).isActive = true
activeExerciseTimerUIView.layer.cornerRadius = activeExerciseTimerUIView.frame.width/2
progressBar.frame = CGRect(
x: 0,
y: 0,
width: activeExerciseTimerUIView.frame.width,
height: activeExerciseTimerUIView.frame.height)
self.activeExerciseTimerUIView.addSubview(progressBar)
}
Below is the Custom ProgressBar Class from source file.
import UIKit
class ProgressBar: UIView, CAAnimationDelegate {
fileprivate var animation = CABasicAnimation()
fileprivate var animationDidStart = false
fileprivate var timerDuration = 0
lazy var fgProgressLayer: CAShapeLayer = {
let fgProgressLayer = CAShapeLayer()
return fgProgressLayer
}()
lazy var bgProgressLayer: CAShapeLayer = {
let bgProgressLayer = CAShapeLayer()
return bgProgressLayer
}()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
loadBgProgressBar()
loadFgProgressBar()
}
override init(frame: CGRect) {
super.init(frame: frame)
loadBgProgressBar()
loadFgProgressBar()
}
fileprivate func loadFgProgressBar() {
let startAngle = CGFloat(-Double.pi / 2)
let endAngle = CGFloat(3 * Double.pi / 2)
let centerPoint = CGPoint(x: frame.width/2 , y: frame.height/2)
let gradientMaskLayer = gradientMask()
fgProgressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: frame.width/2 - 30.0, startAngle:startAngle, endAngle:endAngle, clockwise: true).cgPath
fgProgressLayer.backgroundColor = UIColor.clear.cgColor
fgProgressLayer.fillColor = nil
fgProgressLayer.strokeColor = UIColor.black.cgColor
fgProgressLayer.lineWidth = 4.0
fgProgressLayer.strokeStart = 0.0
fgProgressLayer.strokeEnd = 0.0
gradientMaskLayer.mask = fgProgressLayer
layer.addSublayer(gradientMaskLayer)
}
fileprivate func gradientMask() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.locations = [0.0, 1.0]
let colorTop: AnyObject = CustomColor.lime.cgColor
let colorBottom: AnyObject = CustomColor.lime.cgColor
let arrayOfColors: [AnyObject] = [colorTop, colorBottom]
gradientLayer.colors = arrayOfColors
return gradientLayer
}
fileprivate func loadBgProgressBar() {
let startAngle = CGFloat(-Double.pi / 2)
let endAngle = CGFloat(3 * Double.pi / 2)
let centerPoint = CGPoint(x: frame.width/2 , y: frame.height/2)
let gradientMaskLayer = gradientMaskBg()
bgProgressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: frame.width/2 - 30.0, startAngle:startAngle, endAngle:endAngle, clockwise: true).cgPath
bgProgressLayer.backgroundColor = UIColor.clear.cgColor
bgProgressLayer.fillColor = nil
bgProgressLayer.strokeColor = UIColor.black.cgColor
bgProgressLayer.lineWidth = 4.0
bgProgressLayer.strokeStart = 0.0
bgProgressLayer.strokeEnd = 1.0
gradientMaskLayer.mask = bgProgressLayer
layer.addSublayer(gradientMaskLayer)
}
fileprivate func gradientMaskBg() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.locations = [0.0, 1.0]
let colorTop: AnyObject = CustomColor.strawberry.cgColor
let colorBottom: AnyObject = CustomColor.strawberry.cgColor
let arrayOfColors: [AnyObject] = [colorTop, colorBottom]
gradientLayer.colors = arrayOfColors
return gradientLayer
}
Resolve the issue by loading the background and foreground progress bars in layoutSubViews function.
ProgressBar.swift
import UIKit
import Pulsator
class ProgressBar: UIView, CAAnimationDelegate {
fileprivate var animation = CABasicAnimation()
fileprivate var animationDidStart = false
fileprivate var updatedAnimationAdded = false
fileprivate var updateExecuted = false
fileprivate var totalTimerDuration = 0
fileprivate var isProgressBarAdded:Bool = false
fileprivate var barlineWidth: CGFloat = 8
// let pulsator = Pulsator()
// fileprivate var width: CGFloat
// fileprivate var height: CGFloat
//
lazy var fgProgressLayer: CAShapeLayer = {
let fgProgressLayer = CAShapeLayer()
return fgProgressLayer
}()
lazy var bgProgressLayer: CAShapeLayer = {
let bgProgressLayer = CAShapeLayer()
return bgProgressLayer
}()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
loadBgProgressBar()
loadFgProgressBar()
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func layoutSubviews() {
super.layoutSubviews()
if self.frame.width > 0 && self.frame.height > 0 && isProgressBarAdded == false {
loadBgProgressBar()
loadFgProgressBar()
isProgressBarAdded = true
}
}

Can't get custom activity indicator to animate

I rewrote a custom activity indicator that was originally in an Objc file into Swift. The activity indicator appears on scene but the animation isn't occurring.
I need some help figuring out why the animation isn't occurring:
vc:
class ViewController: UIViewController {
fileprivate lazy var customActivityView: CustomActivityView = {
let customActivityView = CustomActivityView()
customActivityView.translatesAutoresizingMaskIntoConstraints = false
customActivityView.delegate = self
customActivityView.numberOfCircles = 3
customActivityView.radius = 20
customActivityView.internalSpacing = 3
customActivityView.startAnimating()
return customActivityView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
}
fileprivate func setAnchors() {
view.addSubview(customActivityView)
customActivityView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
customActivityView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
customActivityView.widthAnchor.constraint(equalToConstant: 100).isActive = true
customActivityView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
}
extension ViewController: CustomActivityViewDelegate {
func activityIndicatorView(activityIndicatorView: CustomActivityView, circleBackgroundColorAtIndex index: Int) -> UIColor {
let red = CGFloat(Double((arc4random() % 256)) / 255.0)
let green = CGFloat(Double((arc4random() % 256)) / 255.0)
let blue = CGFloat(Double((arc4random() % 256)) / 255.0)
let alpha: CGFloat = 1
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
Swift file:
import UIKit
protocol CustomActivityViewDelegate: class {
func activityIndicatorView(activityIndicatorView: CustomActivityView, circleBackgroundColorAtIndex index: Int) -> UIColor
}
class CustomActivityView: UIView {
var numberOfCircles: Int = 0
var internalSpacing: CGFloat = 0
var radius: CGFloat = 0
var delay: CGFloat = 0
var duration: CFTimeInterval = 0
var defaultColor = UIColor.systemPink
var isAnimating = false
weak var delegate: CustomActivityViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
setupDefaults()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupDefaults()
fatalError("init(coder:) has not been implemented")
}
func setupDefaults() {
self.translatesAutoresizingMaskIntoConstraints = false
numberOfCircles = 5
internalSpacing = 5
radius = 10
delay = 0.2
duration = 0.8
}
func createCircleWithRadius(radius: CGFloat, color: UIColor, positionX: CGFloat) -> UIView {
let circle = UIView(frame: CGRect(x: positionX, y: 0, width: radius * 2, height: radius * 2))
circle.backgroundColor = color
circle.layer.cornerRadius = radius
circle.translatesAutoresizingMaskIntoConstraints = false;
return circle
}
func createAnimationWithDuration(duration: CFTimeInterval, delay: CGFloat) -> CABasicAnimation {
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
anim.fromValue = 0.0
anim.toValue = 1.0
anim.autoreverses = true
anim.duration = duration
anim.isRemovedOnCompletion = false
anim.beginTime = CACurrentMediaTime()+Double(delay)
anim.repeatCount = .infinity
anim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
return anim;
}
func addCircles() {
for i in 0..<numberOfCircles {
var color: UIColor?
color = delegate?.activityIndicatorView(activityIndicatorView: self, circleBackgroundColorAtIndex: i)
let circle = createCircleWithRadius(radius: radius,
color: ((color == nil) ? self.defaultColor : color)!,
positionX: CGFloat(i) * ((2 * self.radius) + self.internalSpacing))
circle.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
circle.layer.add(self.createAnimationWithDuration(duration: self.duration,
delay: CGFloat(i) * self.delay), forKey: "scale")
self.addSubview(circle)
}
}
func removeCircles() {
self.subviews.forEach({ $0.removeFromSuperview() })
}
#objc func startAnimating() {
if !isAnimating {
addCircles()
self.isHidden = false
isAnimating = true
}
}
#objc func stopAnimating() {
if isAnimating {
removeCircles()
self.isHidden = true
isAnimating = false
}
}
func intrinsicContentSize() -> CGSize {
let width: CGFloat = (CGFloat(self.numberOfCircles) * ((2 * self.radius) + self.internalSpacing)) - self.internalSpacing
let height: CGFloat = radius * 2
return CGSize(width: width, height: height)
}
func setNumberOfCircles(numberOfCircles: Int) {
self.numberOfCircles = numberOfCircles
self.invalidateIntrinsicContentSize()
}
func setRadius(radius: CGFloat) {
self.radius = radius
self.invalidateIntrinsicContentSize()
}
func setInternalSpacing(internalSpacing: CGFloat) {
self.internalSpacing = internalSpacing
self.invalidateIntrinsicContentSize()
}
}
I used the wrong key path for the animation:
I used
// incorrect
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
but I should've used
// correct
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")

IOS Step menu using swift

I would like to create the stepper menu in IOS using swift, But I'm facing some issues. Here are the issues.
1) Portrait and landscape stepper menu is not propper.
2) How to set default step position with the method below method, It's working when button clicked. But I want to set when menu loads the first time.
self.stepView.setSelectedPosition(index: 2)
3) If it reached the position last, I would like to change the color for complete path parentPathRect.
4) Progress animation CABasicAnimation is not like the progress bar, I want to show the animation.
5) It should not remove the selected position color when changing the orientation.
As per my organization rules should not use third-party frameworks.
Can anyone help me with the solution? Or is there any alternative solution for this?
Here is my code:
import UIKit
class ViewController: UIViewController, StepMenuDelegate {
#IBOutlet weak var stepView: StepView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.stepView.delegate = self;
self.stepView.titles = ["1", "2", "3"]
self.stepView.lineWidth = 8
self.stepView.offSet = 8
self.stepView.setSelectedPosition(index: 2)
}
func didSelectItem(atIndex index: NSInteger) {
print(index)
}
}
protocol StepMenuDelegate {
func didSelectItem(atIndex index: NSInteger)
}
class StepView: UIView {
var delegate : StepMenuDelegate!
var titles: [String] = [] {
didSet(values) {
setup()
setupItems()
}
}
var lineWidth: CGFloat = 8 {
didSet(values) {
setup()
}
}
var offSet: CGFloat = 8 {
didSet(values) {
self.itemOffset = offSet * 4
setup()
}
}
private var selectedIndex : NSInteger!
private var itemOffset : CGFloat = 8 {
didSet (value) {
setup()
setupItems()
}
}
private var path : UIBezierPath!
var selectedLayer : CAShapeLayer!
private var parentPathRect : CGRect!
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
self.setup()
setupItems()
}
func setup() {
self.removeAllButtonsAndLayes()
let layer = CAShapeLayer()
self.parentPathRect = CGRect(origin: CGPoint(x: offSet, y: self.bounds.midY - (self.lineWidth/2) ), size: CGSize(width: self.bounds.width - (offSet * 2), height: lineWidth))
path = UIBezierPath(roundedRect: self.parentPathRect, cornerRadius: 2)
layer.path = path.cgPath
layer.fillColor = UIColor.orange.cgColor
layer.lineCap = .butt
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowOffset = CGSize(width: 1, height: 2)
layer.shadowOpacity = 0.1
layer.shadowRadius = 2
self.layer.addSublayer(layer)
}
func setupItems() {
removeAllButtonsAndLayes()
let itemRect = CGRect(x: self.itemOffset, y: 0, width: 34, height: 34)
let totalWidth = self.bounds.width
let itemWidth = totalWidth / CGFloat(self.titles.count);
for i in 0..<self.titles.count {
let button = UIButton()
var xPos: CGFloat = itemOffset
self.addSubview(button)
xPos += (CGFloat(i) * itemWidth);
xPos += itemOffset/3
button.translatesAutoresizingMaskIntoConstraints = false
button.leftAnchor.constraint(equalTo: self.leftAnchor, constant: xPos).isActive = true
button.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0).isActive = true
button.heightAnchor.constraint(equalToConstant: itemRect.height).isActive = true
button.widthAnchor.constraint(equalToConstant: itemRect.width).isActive = true
button.backgroundColor = UIColor.red
button.layer.zPosition = 1
button.layer.cornerRadius = itemRect.height/2
let name : String = self.titles[i]
button.tag = i
button.setTitle(name, for: .normal)
button.addTarget(self, action: #selector(selectedItemEvent(sender:)), for: .touchUpInside)
if self.selectedIndex != nil {
if button.tag == self.selectedIndex {
selectedItemEvent(sender: button)
}
}
}
}
#objc func selectedItemEvent(sender:UIButton) {
if self.selectedLayer != nil {
selectedLayer.removeFromSuperlayer()
}
self.delegate.didSelectItem(atIndex: sender.tag)
let fromRect = self.parentPathRect.origin
self.selectedLayer = CAShapeLayer()
let rect = CGRect(origin: fromRect, size: CGSize(width:sender.frame.origin.x - 4, height: 8))
let path = UIBezierPath(roundedRect: rect, cornerRadius: 4)
self.selectedLayer.path = path.cgPath
self.selectedLayer.lineCap = .round
self.selectedLayer.fillColor = UIColor.orange.cgColor
let animation = CABasicAnimation(keyPath: "fillColor")
animation.toValue = UIColor.blue.cgColor
animation.duration = 0.2
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.selectedLayer.add(animation, forKey: "fillColor")
self.layer.addSublayer(self.selectedLayer)
}
func removeAllButtonsAndLayes() {
for button in self.subviews {
if button is UIButton {
button.removeFromSuperview()
}
}
}
func setSelectedPosition(index:NSInteger) {
self.selectedIndex = index
}
}
Here I found One way to achieve the solution.!!
https://gist.github.com/DamodarGit/7f0f484708f60c996772ae28e5e1c615
Welcome to suggestions or code changes.!!

Programmatically created UIView's frame is 0

Cannot access uiview's frame after setting it's layout. Frame, and center is given as 0,0 points.
I should also mention that there are no storyboard's in this project. All views and everything are created programmatically.
I have created a UIView resultView programmatically and added it as a subview in scrollView, which is also added as a subview of view, then set it's constraints, anchors in a method called setupLayout() I call setupLayout() in viewDidLoad() and after that method, I call another method called configureShapeLayer(). Inside configureShapeLayer() I try to access my view's center as:
let center = resultView.center // should give resultView's center but gives 0
Then by using this center value I try to add two bezierPaths to have a status bar kind of view. But since resultView's center is not updated at that point, it appears as misplaced. Please see the pic below:
I also tried calling setupLayout() in loadView() then calling configureShapeLayer() in viewDidLoad() but nothing changed.
So I need a way to make sure all views are set in my view, all constraints, and layouts are applied before calling configureShapeLayer(). But how can I do this?
I also tried calling configureShapeLayer() in both viewWillLayoutSubviews() and viewDidLayoutSubviews() methods but it made it worse, and didnt work either.
Whole View Controller File is given below: First views are declared, then they are added into the view in prepareUI(), at the end of prepareUI(), another method setupLayout() is called. After it completes setting layout, as can be seen from viewDidLoad, finally configureShapeLayer() method is called.
import UIKit
class TryViewController: UIViewController {
let score: CGFloat = 70
lazy var percentage: CGFloat = {
return score / 100
}()
// MARK: View Declarations
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .white
return scrollView
}()
private let iconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let percentageLabel: UILabel = {
let label = UILabel()
label.text = ""
label.textAlignment = .center
label.font = TextStyle.systemFont(ofSize: 50.0)
return label
}()
// This one is the one should have status bar at center.
private let resultView: UIView = {
let view = UIView()
view.backgroundColor = .purple
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
prepareUI()
configureShapeLayer()
}
private func prepareUI() {
resultView.addSubviews(views: percentageLabel)
scrollView.addSubviews(views: iconImageView,
resultView)
view.addSubviews(views: scrollView)
setupLayout()
}
private func setupLayout() {
scrollView.fillSuperview()
iconImageView.anchor(top: scrollView.topAnchor,
padding: .init(topPadding: 26.0))
iconImageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.31).isActive = true
iconImageView.heightAnchor.constraint(equalTo: iconImageView.widthAnchor, multiplier: 0.67).isActive = true
iconImageView.anchorCenterXToSuperview()
//percentageLabel.frame = CGRect(x: 0, y: 0, width: 105, height: 60)
//percentageLabel.center = resultView.center
percentageLabel.anchorCenterXToSuperview()
percentageLabel.anchorCenterYToSuperview()
let resultViewTopConstraintRatio: CGFloat = 0.104
resultView.anchor(top: iconImageView.bottomAnchor,
padding: .init(topPadding: (view.frame.height * resultViewTopConstraintRatio)))
resultView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.533).isActive = true
resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor, multiplier: 1.0).isActive = true
resultView.anchorCenterXToSuperview()
configureShapeLayer()
}
private func configureShapeLayer() {
let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2
let center = resultView.center // should give resultView's center but gives 0
// Track Layer Part
let trackPath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = trackPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
resultView.layer.addSublayer(trackLayer)
// Score Fill Part
let scorePath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)
scoreLayer.path = scorePath.cgPath
scoreLayer.strokeColor = UIColor.red.cgColor
scoreLayer.lineWidth = 10
scoreLayer.fillColor = UIColor.clear.cgColor
scoreLayer.lineCap = .round
scoreLayer.strokeEnd = 0
resultView.layer.addSublayer(scoreLayer)
}
}
You will be much better off creating a custom view. That will allow you to "automatically" update your bezier paths when the view size changes.
It also allows you to keep your drawing code away from your controller code.
Here is a simple example. It adds a button above the "resultView" - each time it's tapped it will increment the percentage by 5 (percentage starts at 5 for demonstration):
//
// PCTViewController.swift
//
// Created by Don Mag on 12/6/18.
//
import UIKit
class MyResultView: UIView {
let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
var percentage = CGFloat(0.0) {
didSet {
self.percentageLabel.text = "\(Int(percentage * 100))%"
self.setNeedsLayout()
}
}
let percentageLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = ""
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 40.0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(trackLayer)
layer.addSublayer(scoreLayer)
addSubview(percentageLabel)
NSLayoutConstraint.activate([
percentageLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
percentageLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
override func layoutSubviews() {
super.layoutSubviews()
let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2
trackLayer.frame = self.bounds
scoreLayer.frame = self.bounds
let centerPoint = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
// Track Layer Part
let trackPath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = trackPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
// pre-Swift 4.2
trackLayer.lineCap = kCALineCapRound
// trackLayer.lineCap = .round
// Score Fill Part
let scorePath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)
scoreLayer.path = scorePath.cgPath
scoreLayer.strokeColor = UIColor.red.cgColor
scoreLayer.lineWidth = 10
scoreLayer.fillColor = UIColor.clear.cgColor
// pre-Swift 4.2
scoreLayer.lineCap = kCALineCapRound
// scoreLayer.lineCap = .round
}
}
class PCTViewController: UIViewController {
let resultView: MyResultView = {
let v = MyResultView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .purple
return v
}()
let btn: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Add 5 percent", for: .normal)
b.backgroundColor = .blue
return b
}()
var pct = 5
#objc func incrementPercent(_ sender: Any) {
pct += 5
resultView.percentage = CGFloat(pct) / 100.0
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(btn)
view.addSubview(resultView)
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultView.widthAnchor.constraint(equalToConstant: 300.0),
resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor),
resultView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
btn.addTarget(self, action: #selector(incrementPercent), for: .touchUpInside)
resultView.percentage = CGFloat(pct) / 100.0
}
}
Result:
This is not the ideal fix, but try calling configureShapeLayer() func on main thread, like this:
DispatchQueue.main.async {
configureShapeLayer()
}
I had problem like that once and was something like that.

Resources